You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

759 lines
19 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2015-2016 Cirilo Bernardo <cirilo.bernardo@gmail.com>
  5. * Copyright (C) 2018-2022 KiCad Developers, see AUTHORS.txt for contributors.
  6. * Copyright (C) 2022 CERN
  7. *
  8. * This program is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU General Public License
  10. * as published by the Free Software Foundation; either version 2
  11. * of the License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with this program; if not, you may find one here:
  20. * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  21. * or you may search the http://www.gnu.org website for the version 2 license,
  22. * or you may write to the Free Software Foundation, Inc.,
  23. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
  24. */
  25. #define GLM_FORCE_RADIANS
  26. #include <mutex>
  27. #include <utility>
  28. #include <wx/datetime.h>
  29. #include <wx/dir.h>
  30. #include <wx/log.h>
  31. #include <wx/stdpaths.h>
  32. #include <boost/version.hpp>
  33. #if BOOST_VERSION >= 106800
  34. #include <boost/uuid/detail/sha1.hpp>
  35. #else
  36. #include <boost/uuid/sha1.hpp>
  37. #endif
  38. #include "3d_cache.h"
  39. #include "3d_info.h"
  40. #include "3d_plugin_manager.h"
  41. #include "sg/scenegraph.h"
  42. #include "plugins/3dapi/ifsg_api.h"
  43. #include <advanced_config.h>
  44. #include <common.h> // For ExpandEnvVarSubstitutions
  45. #include <filename_resolver.h>
  46. #include <paths.h>
  47. #include <pgm_base.h>
  48. #include <project.h>
  49. #include <settings/common_settings.h>
  50. #include <settings/settings_manager.h>
  51. #include <wx_filename.h>
  52. #define MASK_3D_CACHE "3D_CACHE"
  53. static std::mutex mutex3D_cache;
  54. static std::mutex mutex3D_cacheManager;
  55. static bool isSHA1Same( const unsigned char* shaA, const unsigned char* shaB ) noexcept
  56. {
  57. for( int i = 0; i < 20; ++i )
  58. {
  59. if( shaA[i] != shaB[i] )
  60. return false;
  61. }
  62. return true;
  63. }
  64. static bool checkTag( const char* aTag, void* aPluginMgrPtr )
  65. {
  66. if( nullptr == aTag || nullptr == aPluginMgrPtr )
  67. return false;
  68. S3D_PLUGIN_MANAGER *pp = (S3D_PLUGIN_MANAGER*) aPluginMgrPtr;
  69. return pp->CheckTag( aTag );
  70. }
  71. static const wxString sha1ToWXString( const unsigned char* aSHA1Sum )
  72. {
  73. unsigned char uc;
  74. unsigned char tmp;
  75. char sha1[41];
  76. int j = 0;
  77. for( int i = 0; i < 20; ++i )
  78. {
  79. uc = aSHA1Sum[i];
  80. tmp = uc / 16;
  81. if( tmp > 9 )
  82. tmp += 87;
  83. else
  84. tmp += 48;
  85. sha1[j++] = tmp;
  86. tmp = uc % 16;
  87. if( tmp > 9 )
  88. tmp += 87;
  89. else
  90. tmp += 48;
  91. sha1[j++] = tmp;
  92. }
  93. sha1[j] = 0;
  94. return wxString::FromUTF8Unchecked( sha1 );
  95. }
  96. class S3D_CACHE_ENTRY
  97. {
  98. public:
  99. S3D_CACHE_ENTRY();
  100. ~S3D_CACHE_ENTRY();
  101. void SetSHA1( const unsigned char* aSHA1Sum );
  102. const wxString GetCacheBaseName();
  103. wxDateTime modTime; // file modification time
  104. unsigned char sha1sum[20];
  105. std::string pluginInfo; // PluginName:Version string
  106. SCENEGRAPH* sceneData;
  107. S3DMODEL* renderData;
  108. private:
  109. // prohibit assignment and default copy constructor
  110. S3D_CACHE_ENTRY( const S3D_CACHE_ENTRY& source );
  111. S3D_CACHE_ENTRY& operator=( const S3D_CACHE_ENTRY& source );
  112. wxString m_CacheBaseName; // base name of cache file (a SHA1 digest)
  113. };
  114. S3D_CACHE_ENTRY::S3D_CACHE_ENTRY()
  115. {
  116. sceneData = nullptr;
  117. renderData = nullptr;
  118. memset( sha1sum, 0, 20 );
  119. }
  120. S3D_CACHE_ENTRY::~S3D_CACHE_ENTRY()
  121. {
  122. delete sceneData;
  123. if( nullptr != renderData )
  124. S3D::Destroy3DModel( &renderData );
  125. }
  126. void S3D_CACHE_ENTRY::SetSHA1( const unsigned char* aSHA1Sum )
  127. {
  128. if( nullptr == aSHA1Sum )
  129. {
  130. wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * [BUG] NULL passed for aSHA1Sum" ),
  131. __FILE__, __FUNCTION__, __LINE__ );
  132. return;
  133. }
  134. memcpy( sha1sum, aSHA1Sum, 20 );
  135. }
  136. const wxString S3D_CACHE_ENTRY::GetCacheBaseName()
  137. {
  138. if( m_CacheBaseName.empty() )
  139. m_CacheBaseName = sha1ToWXString( sha1sum );
  140. return m_CacheBaseName;
  141. }
  142. S3D_CACHE::S3D_CACHE()
  143. {
  144. m_FNResolver = new FILENAME_RESOLVER;
  145. m_project = nullptr;
  146. m_Plugins = new S3D_PLUGIN_MANAGER;
  147. }
  148. S3D_CACHE::~S3D_CACHE()
  149. {
  150. FlushCache();
  151. delete m_FNResolver;
  152. delete m_Plugins;
  153. }
  154. SCENEGRAPH* S3D_CACHE::load( const wxString& aModelFile, const wxString& aBasePath,
  155. S3D_CACHE_ENTRY** aCachePtr )
  156. {
  157. if( aCachePtr )
  158. *aCachePtr = nullptr;
  159. wxString full3Dpath = m_FNResolver->ResolvePath( aModelFile, aBasePath );
  160. if( full3Dpath.empty() )
  161. {
  162. // the model cannot be found; we cannot proceed
  163. wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * [3D model] could not find model '%s'\n" ),
  164. __FILE__, __FUNCTION__, __LINE__, aModelFile );
  165. return nullptr;
  166. }
  167. // check cache if file is already loaded
  168. std::lock_guard<std::mutex> lock( mutex3D_cache );
  169. std::map< wxString, S3D_CACHE_ENTRY*, rsort_wxString >::iterator mi;
  170. mi = m_CacheMap.find( full3Dpath );
  171. if( mi != m_CacheMap.end() )
  172. {
  173. wxFileName fname( full3Dpath );
  174. if( fname.FileExists() ) // Only check if file exists. If not, it will
  175. { // use the same model in cache.
  176. bool reload = ADVANCED_CFG::GetCfg().m_Skip3DModelMemoryCache;
  177. wxDateTime fmdate = fname.GetModificationTime();
  178. if( fmdate != mi->second->modTime )
  179. {
  180. unsigned char hashSum[20];
  181. getSHA1( full3Dpath, hashSum );
  182. mi->second->modTime = fmdate;
  183. if( !isSHA1Same( hashSum, mi->second->sha1sum ) )
  184. {
  185. mi->second->SetSHA1( hashSum );
  186. reload = true;
  187. }
  188. }
  189. if( reload )
  190. {
  191. if( nullptr != mi->second->sceneData )
  192. {
  193. S3D::DestroyNode( mi->second->sceneData );
  194. mi->second->sceneData = nullptr;
  195. }
  196. if( nullptr != mi->second->renderData )
  197. S3D::Destroy3DModel( &mi->second->renderData );
  198. mi->second->sceneData = m_Plugins->Load3DModel( full3Dpath,
  199. mi->second->pluginInfo );
  200. }
  201. }
  202. if( nullptr != aCachePtr )
  203. *aCachePtr = mi->second;
  204. return mi->second->sceneData;
  205. }
  206. // a cache item does not exist; search the Filename->Cachename map
  207. return checkCache( full3Dpath, aCachePtr );
  208. }
  209. SCENEGRAPH* S3D_CACHE::Load( const wxString& aModelFile, const wxString& aBasePath )
  210. {
  211. return load( aModelFile, aBasePath );
  212. }
  213. SCENEGRAPH* S3D_CACHE::checkCache( const wxString& aFileName, S3D_CACHE_ENTRY** aCachePtr )
  214. {
  215. if( aCachePtr )
  216. *aCachePtr = nullptr;
  217. unsigned char sha1sum[20];
  218. S3D_CACHE_ENTRY* ep = new S3D_CACHE_ENTRY;
  219. m_CacheList.push_back( ep );
  220. wxFileName fname( aFileName );
  221. ep->modTime = fname.GetModificationTime();
  222. if( !getSHA1( aFileName, sha1sum ) || m_CacheDir.empty() )
  223. {
  224. // just in case we can't get a hash digest (for example, on access issues)
  225. // or we do not have a configured cache file directory, we create an
  226. // entry to prevent further attempts at loading the file
  227. if( m_CacheMap.emplace( aFileName, ep ).second == false )
  228. {
  229. wxLogTrace( MASK_3D_CACHE,
  230. wxT( "%s:%s:%d\n * [BUG] duplicate entry in map file; key = '%s'" ),
  231. __FILE__, __FUNCTION__, __LINE__, aFileName );
  232. m_CacheList.pop_back();
  233. delete ep;
  234. }
  235. else
  236. {
  237. if( aCachePtr )
  238. *aCachePtr = ep;
  239. }
  240. return nullptr;
  241. }
  242. if( m_CacheMap.emplace( aFileName, ep ).second == false )
  243. {
  244. wxLogTrace( MASK_3D_CACHE,
  245. wxT( "%s:%s:%d\n * [BUG] duplicate entry in map file; key = '%s'" ),
  246. __FILE__, __FUNCTION__, __LINE__, aFileName );
  247. m_CacheList.pop_back();
  248. delete ep;
  249. return nullptr;
  250. }
  251. if( aCachePtr )
  252. *aCachePtr = ep;
  253. ep->SetSHA1( sha1sum );
  254. wxString bname = ep->GetCacheBaseName();
  255. wxString cachename = m_CacheDir + bname + wxT( ".3dc" );
  256. if( !ADVANCED_CFG::GetCfg().m_Skip3DModelFileCache && wxFileName::FileExists( cachename )
  257. && loadCacheData( ep ) )
  258. return ep->sceneData;
  259. ep->sceneData = m_Plugins->Load3DModel( aFileName, ep->pluginInfo );
  260. if( !ADVANCED_CFG::GetCfg().m_Skip3DModelFileCache && nullptr != ep->sceneData )
  261. saveCacheData( ep );
  262. return ep->sceneData;
  263. }
  264. bool S3D_CACHE::getSHA1( const wxString& aFileName, unsigned char* aSHA1Sum )
  265. {
  266. if( aFileName.empty() )
  267. {
  268. wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * [BUG] empty filename" ),
  269. __FILE__, __FUNCTION__, __LINE__ );
  270. return false;
  271. }
  272. if( nullptr == aSHA1Sum )
  273. {
  274. wxLogTrace( MASK_3D_CACHE, wxT( "%s\n * [BUG] NULL pointer passed for aMD5Sum" ),
  275. __FILE__, __FUNCTION__, __LINE__ );
  276. return false;
  277. }
  278. #ifdef _WIN32
  279. FILE* fp = _wfopen( aFileName.wc_str(), L"rb" );
  280. #else
  281. FILE* fp = fopen( aFileName.ToUTF8(), "rb" );
  282. #endif
  283. if( nullptr == fp )
  284. return false;
  285. boost::uuids::detail::sha1 dblock;
  286. unsigned char block[4096];
  287. size_t bsize = 0;
  288. while( ( bsize = fread( &block, 1, 4096, fp ) ) > 0 )
  289. dblock.process_bytes( block, bsize );
  290. fclose( fp );
  291. unsigned int digest[5];
  292. dblock.get_digest( digest );
  293. // ensure MSB order
  294. for( int i = 0; i < 5; ++i )
  295. {
  296. int idx = i << 2;
  297. unsigned int tmp = digest[i];
  298. aSHA1Sum[idx+3] = tmp & 0xff;
  299. tmp >>= 8;
  300. aSHA1Sum[idx+2] = tmp & 0xff;
  301. tmp >>= 8;
  302. aSHA1Sum[idx+1] = tmp & 0xff;
  303. tmp >>= 8;
  304. aSHA1Sum[idx] = tmp & 0xff;
  305. }
  306. return true;
  307. }
  308. bool S3D_CACHE::loadCacheData( S3D_CACHE_ENTRY* aCacheItem )
  309. {
  310. wxString bname = aCacheItem->GetCacheBaseName();
  311. if( bname.empty() )
  312. {
  313. wxLogTrace( MASK_3D_CACHE,
  314. wxT( " * [3D model] cannot load cached model; no file hash available" ) );
  315. return false;
  316. }
  317. if( m_CacheDir.empty() )
  318. {
  319. wxLogTrace( MASK_3D_CACHE,
  320. wxT( " * [3D model] cannot load cached model; config directory unknown" ) );
  321. return false;
  322. }
  323. wxString fname = m_CacheDir + bname + wxT( ".3dc" );
  324. if( !wxFileName::FileExists( fname ) )
  325. {
  326. wxLogTrace( MASK_3D_CACHE, wxT( " * [3D model] cannot open file '%s'" ), fname.GetData() );
  327. return false;
  328. }
  329. if( nullptr != aCacheItem->sceneData )
  330. S3D::DestroyNode( (SGNODE*) aCacheItem->sceneData );
  331. aCacheItem->sceneData = (SCENEGRAPH*)S3D::ReadCache( fname.ToUTF8(), m_Plugins, checkTag );
  332. if( nullptr == aCacheItem->sceneData )
  333. return false;
  334. return true;
  335. }
  336. bool S3D_CACHE::saveCacheData( S3D_CACHE_ENTRY* aCacheItem )
  337. {
  338. if( nullptr == aCacheItem )
  339. {
  340. wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * NULL passed for aCacheItem" ),
  341. __FILE__, __FUNCTION__, __LINE__ );
  342. return false;
  343. }
  344. if( nullptr == aCacheItem->sceneData )
  345. {
  346. wxLogTrace( MASK_3D_CACHE, wxT( "%s:%s:%d\n * aCacheItem has no valid scene data" ),
  347. __FILE__, __FUNCTION__, __LINE__ );
  348. return false;
  349. }
  350. wxString bname = aCacheItem->GetCacheBaseName();
  351. if( bname.empty() )
  352. {
  353. wxLogTrace( MASK_3D_CACHE,
  354. wxT( " * [3D model] cannot load cached model; no file hash available" ) );
  355. return false;
  356. }
  357. if( m_CacheDir.empty() )
  358. {
  359. wxLogTrace( MASK_3D_CACHE,
  360. wxT( " * [3D model] cannot load cached model; config directory unknown" ) );
  361. return false;
  362. }
  363. wxString fname = m_CacheDir + bname + wxT( ".3dc" );
  364. if( wxFileName::Exists( fname ) )
  365. {
  366. if( !wxFileName::FileExists( fname ) )
  367. {
  368. wxLogTrace( MASK_3D_CACHE,
  369. wxT( " * [3D model] path exists but is not a regular file '%s'" ), fname );
  370. return false;
  371. }
  372. }
  373. return S3D::WriteCache( fname.ToUTF8(), true, (SGNODE*)aCacheItem->sceneData,
  374. aCacheItem->pluginInfo.c_str() );
  375. }
  376. bool S3D_CACHE::Set3DConfigDir( const wxString& aConfigDir )
  377. {
  378. if( !m_ConfigDir.empty() )
  379. return false;
  380. wxFileName cfgdir( ExpandEnvVarSubstitutions( aConfigDir, m_project ), wxEmptyString );
  381. cfgdir.Normalize( FN_NORMALIZE_FLAGS );
  382. if( !cfgdir.DirExists() )
  383. {
  384. cfgdir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
  385. if( !cfgdir.DirExists() )
  386. {
  387. wxLogTrace( MASK_3D_CACHE,
  388. wxT( "%s:%s:%d\n * failed to create 3D configuration directory '%s'" ),
  389. __FILE__, __FUNCTION__, __LINE__, cfgdir.GetPath() );
  390. return false;
  391. }
  392. }
  393. m_ConfigDir = cfgdir.GetPath();
  394. // inform the file resolver of the config directory
  395. if( !m_FNResolver->Set3DConfigDir( m_ConfigDir ) )
  396. {
  397. wxLogTrace( MASK_3D_CACHE,
  398. wxT( "%s:%s:%d\n * could not set 3D Config Directory on filename resolver\n"
  399. " * config directory: '%s'" ),
  400. __FILE__, __FUNCTION__, __LINE__, m_ConfigDir );
  401. }
  402. // 3D cache data must go to a user's cache directory;
  403. // unfortunately wxWidgets doesn't seem to provide
  404. // functions to retrieve such a directory.
  405. //
  406. // 1. OSX: ~/Library/Caches/kicad/3d/
  407. // 2. Linux: ${XDG_CACHE_HOME}/kicad/3d ~/.cache/kicad/3d/
  408. // 3. MSWin: AppData\Local\kicad\3d
  409. wxFileName cacheDir;
  410. cacheDir.AssignDir( PATHS::GetUserCachePath() );
  411. cacheDir.AppendDir( wxT( "3d" ) );
  412. if( !cacheDir.DirExists() )
  413. {
  414. cacheDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
  415. if( !cacheDir.DirExists() )
  416. {
  417. wxLogTrace( MASK_3D_CACHE,
  418. wxT( "%s:%s:%d\n * failed to create 3D cache directory '%s'" ),
  419. __FILE__, __FUNCTION__, __LINE__, cacheDir.GetPath() );
  420. return false;
  421. }
  422. }
  423. m_CacheDir = cacheDir.GetPathWithSep();
  424. return true;
  425. }
  426. bool S3D_CACHE::SetProject( PROJECT* aProject )
  427. {
  428. m_project = aProject;
  429. bool hasChanged = false;
  430. if( m_FNResolver->SetProject( aProject, &hasChanged ) && hasChanged )
  431. {
  432. m_CacheMap.clear();
  433. std::list< S3D_CACHE_ENTRY* >::iterator sL = m_CacheList.begin();
  434. std::list< S3D_CACHE_ENTRY* >::iterator eL = m_CacheList.end();
  435. while( sL != eL )
  436. {
  437. delete *sL;
  438. ++sL;
  439. }
  440. m_CacheList.clear();
  441. return true;
  442. }
  443. return false;
  444. }
  445. void S3D_CACHE::SetProgramBase( PGM_BASE* aBase )
  446. {
  447. m_FNResolver->SetProgramBase( aBase );
  448. }
  449. FILENAME_RESOLVER* S3D_CACHE::GetResolver() noexcept
  450. {
  451. return m_FNResolver;
  452. }
  453. std::list< wxString > const* S3D_CACHE::GetFileFilters() const
  454. {
  455. return m_Plugins->GetFileFilters();
  456. }
  457. void S3D_CACHE::FlushCache( bool closePlugins )
  458. {
  459. std::list< S3D_CACHE_ENTRY* >::iterator sCL = m_CacheList.begin();
  460. std::list< S3D_CACHE_ENTRY* >::iterator eCL = m_CacheList.end();
  461. while( sCL != eCL )
  462. {
  463. delete *sCL;
  464. ++sCL;
  465. }
  466. m_CacheList.clear();
  467. m_CacheMap.clear();
  468. if( closePlugins )
  469. ClosePlugins();
  470. }
  471. void S3D_CACHE::ClosePlugins()
  472. {
  473. if( m_Plugins )
  474. m_Plugins->ClosePlugins();
  475. }
  476. S3DMODEL* S3D_CACHE::GetModel( const wxString& aModelFileName, const wxString& aBasePath )
  477. {
  478. S3D_CACHE_ENTRY* cp = nullptr;
  479. SCENEGRAPH* sp = load( aModelFileName, aBasePath,&cp );
  480. if( !sp )
  481. return nullptr;
  482. if( !cp )
  483. {
  484. wxLogTrace( MASK_3D_CACHE,
  485. wxT( "%s:%s:%d\n * [BUG] model loaded with no associated S3D_CACHE_ENTRY" ),
  486. __FILE__, __FUNCTION__, __LINE__ );
  487. return nullptr;
  488. }
  489. if( cp->renderData )
  490. return cp->renderData;
  491. S3DMODEL* mp = S3D::GetModel( sp );
  492. cp->renderData = mp;
  493. return mp;
  494. }
  495. void S3D_CACHE::CleanCacheDir( int aNumDaysOld )
  496. {
  497. wxDir dir;
  498. wxString fileSpec = wxT( "*.3dc" );
  499. wxArrayString fileList; // Holds list of ".3dc" files found in cache directory
  500. size_t numFilesFound = 0;
  501. wxFileName thisFile;
  502. wxDateTime lastAccess, thresholdDate;
  503. wxDateSpan durationInDays;
  504. // Calc the threshold date above which we delete cache files
  505. durationInDays.SetDays( aNumDaysOld );
  506. thresholdDate = wxDateTime::Now() - durationInDays;
  507. // If the cache directory can be found and opened, then we'll try and clean it up
  508. if( dir.Open( m_CacheDir ) )
  509. {
  510. thisFile.SetPath( m_CacheDir ); // Set the base path to the cache folder
  511. // Get a list of all the ".3dc" files in the cache directory
  512. numFilesFound = dir.GetAllFiles( m_CacheDir, &fileList, fileSpec );
  513. for( unsigned int i = 0; i < numFilesFound; i++ )
  514. {
  515. // Completes path to specific file so we can get its "last access" date
  516. thisFile.SetFullName( fileList[i] );
  517. // Only get "last access" time to compare against. Don't need the other 2 timestamps.
  518. if( thisFile.GetTimes( &lastAccess, nullptr, nullptr ) )
  519. {
  520. if( lastAccess.IsEarlierThan( thresholdDate ) )
  521. {
  522. // This file is older than the threshold so delete it
  523. wxRemoveFile( thisFile.GetFullPath() );
  524. }
  525. }
  526. }
  527. }
  528. }
  529. void PROJECT::Cleanup3DCache()
  530. {
  531. std::lock_guard<std::mutex> lock( mutex3D_cacheManager );
  532. // Get the existing cache from the project
  533. S3D_CACHE* cache = dynamic_cast<S3D_CACHE*>( GetElem( ELEM_3DCACHE ) );
  534. if( cache )
  535. {
  536. // We'll delete ".3dc" cache files older than this many days
  537. int clearCacheInterval = 0;
  538. if( Pgm().GetCommonSettings() )
  539. clearCacheInterval = Pgm().GetCommonSettings()->m_System.clear_3d_cache_interval;
  540. // An interval of zero means the user doesn't want to ever clear the cache
  541. if( clearCacheInterval > 0 )
  542. cache->CleanCacheDir( clearCacheInterval );
  543. }
  544. }
  545. S3D_CACHE* PROJECT::Get3DCacheManager( bool aUpdateProjDir )
  546. {
  547. std::lock_guard<std::mutex> lock( mutex3D_cacheManager );
  548. // Get the existing cache from the project
  549. S3D_CACHE* cache = dynamic_cast<S3D_CACHE*>( GetElem( ELEM_3DCACHE ) );
  550. if( !cache )
  551. {
  552. // Create a cache if there is not one already
  553. cache = new S3D_CACHE();
  554. wxFileName cfgpath;
  555. cfgpath.AssignDir( SETTINGS_MANAGER::GetUserSettingsPath() );
  556. cfgpath.AppendDir( wxT( "3d" ) );
  557. cache->SetProgramBase( &Pgm() );
  558. cache->Set3DConfigDir( cfgpath.GetFullPath() );
  559. SetElem( ELEM_3DCACHE, cache );
  560. aUpdateProjDir = true;
  561. }
  562. if( aUpdateProjDir )
  563. cache->SetProject( this );
  564. return cache;
  565. }
  566. FILENAME_RESOLVER* PROJECT::Get3DFilenameResolver()
  567. {
  568. return Get3DCacheManager()->GetResolver();
  569. }