FbxExportSettings.cs 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556
  1. using System;
  2. using System.IO;
  3. using UnityEditorInternal;
  4. using UnityEngine;
  5. using System.Collections.Generic;
  6. using System.Linq;
  7. using System.Reflection;
  8. using System.Runtime.Serialization;
  9. using System.Security.Permissions;
  10. namespace UnityEditor.Formats.Fbx.Exporter {
  11. [System.Serializable]
  12. internal class FbxExportSettingsException : System.Exception
  13. {
  14. public FbxExportSettingsException() { }
  15. public FbxExportSettingsException(string message)
  16. : base(message) { }
  17. public FbxExportSettingsException(string message, System.Exception inner)
  18. : base(message, inner) { }
  19. protected FbxExportSettingsException(SerializationInfo info, StreamingContext context)
  20. : base(info, context) { }
  21. }
  22. [CustomEditor(typeof(ExportSettings))]
  23. internal class ExportSettingsEditor : UnityEditor.Editor {
  24. Vector2 scrollPos = Vector2.zero;
  25. const float LabelWidth = 144;
  26. const float SelectableLabelMinWidth = 90;
  27. const float BrowseButtonWidth = 25;
  28. const float FieldOffset = 18;
  29. const float BrowseButtonOffset = 5;
  30. static class Style
  31. {
  32. public static GUIContent Application3D = new GUIContent(
  33. "3D Application:",
  34. "Select the 3D Application for which you would like to install the Unity integration.");
  35. public static GUIContent KeepOpen = new GUIContent("Keep Open:",
  36. "Keep the selected 3D application open after Unity integration install has completed.");
  37. public static GUIContent HideNativeMenu = new GUIContent("Hide Native Menu:",
  38. "Replace Maya's native 'Send to Unity' menu with the Unity Integration's menu");
  39. public static GUIContent InstallIntegrationContent = new GUIContent(
  40. "Install Unity Integration",
  41. "Install and configure the Unity integration for the selected 3D application so that you can import and export directly with this project.");
  42. public static GUIContent RepairMissingScripts = new GUIContent(
  43. "Run Component Updater",
  44. "If FBX exporter version 1.3.0f1 or earlier was previously installed, then links to the FbxPrefab component will need updating.\n" +
  45. "Run this to update all FbxPrefab references in text serialized prefabs and scene files.");
  46. }
  47. [SecurityPermission(SecurityAction.LinkDemand)]
  48. public override void OnInspectorGUI() {
  49. ExportSettings exportSettings = (ExportSettings)target;
  50. // Increasing the label width so that none of the text gets cut off
  51. EditorGUIUtility.labelWidth = LabelWidth;
  52. scrollPos = GUILayout.BeginScrollView (scrollPos);
  53. var version = UnityEditor.Formats.Fbx.Exporter.ModelExporter.GetVersionFromReadme ();
  54. if (!string.IsNullOrEmpty(version)) {
  55. GUILayout.Label ("Version: " + version, EditorStyles.centeredGreyMiniLabel);
  56. EditorGUILayout.Space ();
  57. }
  58. GUILayout.BeginVertical();
  59. EditorGUILayout.LabelField("Export Options", EditorStyles.boldLabel);
  60. EditorGUI.indentLevel++;
  61. exportSettings.ShowConvertToPrefabDialog = EditorGUILayout.Toggle(
  62. new GUIContent("Show Convert UI:", "Show the Convert dialog when converting to an FBX Prefab Variant"),
  63. exportSettings.ShowConvertToPrefabDialog
  64. );
  65. EditorGUILayout.Space();
  66. EditorGUILayout.Space();
  67. EditorGUI.indentLevel--;
  68. EditorGUILayout.LabelField("Integration", EditorStyles.boldLabel);
  69. EditorGUI.indentLevel++;
  70. GUILayout.BeginHorizontal ();
  71. EditorGUILayout.LabelField(Style.Application3D, GUILayout.Width(LabelWidth - FieldOffset));
  72. // dropdown to select Maya version to use
  73. var options = ExportSettings.GetDCCOptions();
  74. exportSettings.SelectedDCCApp = EditorGUILayout.Popup(exportSettings.SelectedDCCApp, options);
  75. if (GUILayout.Button(new GUIContent("...", "Browse to a 3D application in a non-default location"), EditorStyles.miniButton, GUILayout.Width(BrowseButtonWidth))) {
  76. var ext = "";
  77. switch (Application.platform) {
  78. case RuntimePlatform.WindowsEditor:
  79. ext = "exe";
  80. break;
  81. case RuntimePlatform.OSXEditor:
  82. ext = "app";
  83. break;
  84. default:
  85. throw new System.NotImplementedException ();
  86. }
  87. string dccPath = EditorUtility.OpenFilePanel ("Select Digital Content Creation Application", ExportSettings.FirstValidVendorLocation, ext);
  88. // check that the path is valid and references the maya executable
  89. if (!string.IsNullOrEmpty (dccPath)) {
  90. ExportSettings.DCCType foundDCC = ExportSettings.DCCType.Maya;
  91. var foundDCCPath = TryFindDCC (dccPath, ext, ExportSettings.DCCType.Maya);
  92. if (foundDCCPath == null && Application.platform == RuntimePlatform.WindowsEditor) {
  93. foundDCCPath = TryFindDCC (dccPath, ext, ExportSettings.DCCType.Max);
  94. foundDCC = ExportSettings.DCCType.Max;
  95. }
  96. if (foundDCCPath == null) {
  97. Debug.LogError (string.Format ("Could not find supported 3D application at: \"{0}\"", Path.GetDirectoryName (dccPath)));
  98. } else {
  99. dccPath = foundDCCPath;
  100. ExportSettings.AddDCCOption (dccPath, foundDCC);
  101. }
  102. Repaint ();
  103. }
  104. }
  105. GUILayout.EndHorizontal ();
  106. EditorGUILayout.Space();
  107. exportSettings.LaunchAfterInstallation = EditorGUILayout.Toggle(
  108. Style.KeepOpen,
  109. exportSettings.LaunchAfterInstallation
  110. );
  111. exportSettings.HideSendToUnityMenuProperty = EditorGUILayout.Toggle(
  112. Style.HideNativeMenu,
  113. exportSettings.HideSendToUnityMenuProperty
  114. );
  115. EditorGUILayout.Space();
  116. // disable button if no 3D application is available
  117. EditorGUI.BeginDisabledGroup (!ExportSettings.CanInstall());
  118. if (GUILayout.Button (Style.InstallIntegrationContent)) {
  119. EditorApplication.delayCall += UnityEditor.Formats.Fbx.Exporter.IntegrationsUI.InstallDCCIntegration;
  120. }
  121. EditorGUI.EndDisabledGroup ();
  122. EditorGUILayout.Space ();
  123. EditorGUI.indentLevel--;
  124. EditorGUILayout.LabelField ("FBX Prefab Component Updater", EditorStyles.boldLabel);
  125. EditorGUI.indentLevel++;
  126. EditorGUILayout.Space ();
  127. if (GUILayout.Button (Style.RepairMissingScripts)) {
  128. var componentUpdater = new UnityEditor.Formats.Fbx.Exporter.RepairMissingScripts ();
  129. var filesToRepairCount = componentUpdater.AssetsToRepairCount;
  130. var dialogTitle = "FBX Prefab Component Updater";
  131. if (filesToRepairCount > 0) {
  132. bool result = UnityEditor.EditorUtility.DisplayDialog (dialogTitle,
  133. string.Format("Found {0} prefab(s) and/or scene(s) with components requiring update.\n\n" +
  134. "If you choose 'Go Ahead', the FbxPrefab components in these assets " +
  135. "will be automatically updated to work with the latest FBX exporter.\n" +
  136. "You should make a backup before proceeding.", filesToRepairCount),
  137. "I Made a Backup. Go Ahead!", "No Thanks");
  138. if (result) {
  139. componentUpdater.ReplaceGUIDInTextAssets ();
  140. } else {
  141. var assetsToRepair = componentUpdater.GetAssetsToRepair ();
  142. Debug.LogFormat ("Failed to update the FbxPrefab components in the following files:\n{0}", string.Join ("\n", assetsToRepair));
  143. }
  144. }
  145. else
  146. {
  147. UnityEditor.EditorUtility.DisplayDialog(dialogTitle,
  148. "Couldn't find any prefabs or scenes that require updating", "Ok");
  149. }
  150. }
  151. GUILayout.FlexibleSpace ();
  152. GUILayout.EndVertical();
  153. GUILayout.EndScrollView ();
  154. if (GUI.changed) {
  155. EditorUtility.SetDirty (exportSettings);
  156. exportSettings.Save ();
  157. }
  158. }
  159. private static string TryFindDCC(string dccPath, string ext, ExportSettings.DCCType dccType){
  160. string dccName = "";
  161. switch (dccType) {
  162. case ExportSettings.DCCType.Maya:
  163. dccName = "maya";
  164. break;
  165. case ExportSettings.DCCType.Max:
  166. dccName = "3dsmax";
  167. break;
  168. default:
  169. throw new System.NotImplementedException ();
  170. }
  171. if (Path.GetFileNameWithoutExtension (dccPath).ToLower ().Equals (dccName)) {
  172. return dccPath;
  173. }
  174. // clicked on the wrong application, try to see if we can still find
  175. // a dcc in this directory.
  176. var dccDir = new DirectoryInfo(Path.GetDirectoryName(dccPath));
  177. FileSystemInfo[] files = {};
  178. switch(Application.platform){
  179. case RuntimePlatform.OSXEditor:
  180. files = dccDir.GetDirectories ("*." + ext);
  181. break;
  182. case RuntimePlatform.WindowsEditor:
  183. files = dccDir.GetFiles ("*." + ext);
  184. break;
  185. default:
  186. throw new System.NotImplementedException();
  187. }
  188. string newDccPath = null;
  189. foreach (var file in files) {
  190. var filename = Path.GetFileNameWithoutExtension (file.Name).ToLower ();
  191. if (filename.Equals (dccName)) {
  192. newDccPath = file.FullName.Replace("\\","/");
  193. break;
  194. }
  195. }
  196. return newDccPath;
  197. }
  198. [SettingsProvider]
  199. static SettingsProvider CreateFbxExportSettingsProvider()
  200. {
  201. ExportSettings.instance.name = "FBX Export Settings";
  202. ExportSettings.instance.Load();
  203. var provider = AssetSettingsProvider.CreateProviderFromObject(
  204. "Project/Fbx Export", ExportSettings.instance, GetSearchKeywordsFromGUIContentProperties(typeof(Style)));
  205. #if UNITY_2019_1_OR_NEWER
  206. provider.inspectorUpdateHandler += () =>
  207. {
  208. if (provider.settingsEditor != null &&
  209. provider.settingsEditor.serializedObject.UpdateIfRequiredOrScript())
  210. {
  211. provider.Repaint();
  212. }
  213. };
  214. #else
  215. provider.activateHandler += (searchContext, rootElement) =>
  216. {
  217. if (provider.settingsEditor != null &&
  218. provider.settingsEditor.serializedObject.UpdateIfRequiredOrScript())
  219. {
  220. provider.Repaint();
  221. }
  222. };
  223. #endif // UNITY_2019_1_OR_NEWER
  224. return provider;
  225. }
  226. static IEnumerable<string> GetSearchKeywordsFromGUIContentProperties(Type type)
  227. {
  228. return type.GetFields(BindingFlags.Static | BindingFlags.Public)
  229. .Where(field => typeof(GUIContent).IsAssignableFrom(field.FieldType))
  230. .Select(field => ((GUIContent)field.GetValue(null)).text)
  231. .Concat(type.GetProperties(BindingFlags.Static | BindingFlags.Public)
  232. .Where(prop => typeof(GUIContent).IsAssignableFrom(prop.PropertyType))
  233. .Select(prop => ((GUIContent)prop.GetValue(null, null)).text))
  234. .Where(content => content != null)
  235. .Select(content => content.ToLowerInvariant())
  236. .Distinct();
  237. }
  238. }
  239. [FilePath("ProjectSettings/FbxExportSettings.asset",FilePathAttribute.Location.ProjectFolder)]
  240. internal class ExportSettings : ScriptableSingleton<ExportSettings>
  241. {
  242. public enum ExportFormat { ASCII = 0, Binary = 1}
  243. public enum Include { Model = 0, Anim = 1, ModelAndAnim = 2 }
  244. public enum ObjectPosition { LocalCentered = 0, WorldAbsolute = 1, Reset = 2 /* For convert to model only, no UI option*/}
  245. public enum LODExportType { All = 0, Highest = 1, Lowest = 2 }
  246. public bool ExportOutsideProject = false;
  247. internal const string kDefaultSavePath = ".";
  248. private static List<string> s_PreferenceList = new List<string>() {kMayaOptionName, kMayaLtOptionName, kMaxOptionName};
  249. //Any additional names require a space after the name
  250. internal const string kMaxOptionName = "3ds Max ";
  251. internal const string kMayaOptionName = "Maya ";
  252. internal const string kMayaLtOptionName = "Maya LT";
  253. // NOTE: using "Verbose" and "VerboseProperty" to handle backwards compatibility with older FbxExportSettings.asset files.
  254. // The variable name is used when serializing, so changing the variable name would prevent older FbxExportSettings.asset files
  255. // from loading this property.
  256. [SerializeField]
  257. private bool Verbose = false;
  258. internal bool VerboseProperty
  259. {
  260. get { return Verbose; }
  261. set { Verbose = value; }
  262. }
  263. private static string DefaultIntegrationSavePath {
  264. get{
  265. return Path.GetDirectoryName(Application.dataPath);
  266. }
  267. }
  268. private static string GetMayaLocationFromEnvironmentVariable(string env)
  269. {
  270. string result = null;
  271. if (string.IsNullOrEmpty(env))
  272. return null;
  273. string location = Environment.GetEnvironmentVariable(env);
  274. if (string.IsNullOrEmpty(location))
  275. return null;
  276. //Remove any extra slashes on the end
  277. //Maya would accept a single slash in either direction, so we should be able to
  278. location = location.Replace("\\", "/");
  279. location = location.TrimEnd('/');
  280. if (!Directory.Exists(location))
  281. return null;
  282. if (Application.platform == RuntimePlatform.WindowsEditor)
  283. {
  284. //If we are on Windows, we need only go up one location to get to the "Autodesk" folder.
  285. result = Directory.GetParent(location).ToString();
  286. }
  287. else if (Application.platform == RuntimePlatform.OSXEditor)
  288. {
  289. //We can assume our path is: /Applications/Autodesk/maya2017/Maya.app/Contents
  290. //So we need to go up three folders.
  291. var appFolder = Directory.GetParent(location);
  292. if (appFolder != null)
  293. {
  294. var versionFolder = Directory.GetParent(appFolder.ToString());
  295. if (versionFolder != null)
  296. {
  297. var autoDeskFolder = Directory.GetParent(versionFolder.ToString());
  298. if (autoDeskFolder != null)
  299. {
  300. result = autoDeskFolder.ToString();
  301. }
  302. }
  303. }
  304. }
  305. return NormalizePath(result, false);
  306. }
  307. /// <summary>
  308. /// Returns a set of valid vendor folder paths with no trailing '/'
  309. /// </summary>
  310. [SecurityPermission(SecurityAction.LinkDemand)]
  311. private static HashSet<string> GetCustomVendorLocations()
  312. {
  313. HashSet<string> result = null;
  314. var environmentVariable = Environment.GetEnvironmentVariable("UNITY_3DAPP_VENDOR_LOCATIONS");
  315. if (!string.IsNullOrEmpty(environmentVariable))
  316. {
  317. result = new HashSet<string>();
  318. string[] locations = environmentVariable.Split(';');
  319. foreach (var location in locations)
  320. {
  321. if (Directory.Exists(location))
  322. {
  323. result.Add(NormalizePath(location, false));
  324. }
  325. }
  326. }
  327. return result;
  328. }
  329. [SecurityPermission(SecurityAction.LinkDemand)]
  330. private static HashSet<string> GetDefaultVendorLocations()
  331. {
  332. if (Application.platform == RuntimePlatform.WindowsEditor)
  333. {
  334. HashSet<string> windowsDefaults = new HashSet<string>() { "C:/Program Files/Autodesk" };
  335. HashSet<string> existingDirectories = new HashSet<string>();
  336. foreach (string path in windowsDefaults)
  337. {
  338. if (Directory.Exists(path))
  339. {
  340. existingDirectories.Add(path);
  341. }
  342. }
  343. return existingDirectories;
  344. }
  345. else if (Application.platform == RuntimePlatform.OSXEditor)
  346. {
  347. HashSet<string> MacOSDefaults = new HashSet<string>() { "/Applications/Autodesk" };
  348. HashSet<string> existingDirectories = new HashSet<string>();
  349. foreach (string path in MacOSDefaults)
  350. {
  351. if (Directory.Exists(path))
  352. {
  353. existingDirectories.Add(path);
  354. }
  355. }
  356. return existingDirectories;
  357. }
  358. throw new NotImplementedException();
  359. }
  360. /// <summary>
  361. /// Retrieve available vendor locations.
  362. /// If there is valid alternative vendor locations, do not use defaults
  363. /// always use MAYA_LOCATION when available
  364. /// </summary>
  365. internal static List<string> DCCVendorLocations
  366. {
  367. [SecurityPermission(SecurityAction.LinkDemand)]
  368. get
  369. {
  370. HashSet<string> result = GetCustomVendorLocations();
  371. if (result == null)
  372. {
  373. result = GetDefaultVendorLocations();
  374. }
  375. var additionalLocation = GetMayaLocationFromEnvironmentVariable("MAYA_LOCATION");
  376. if (!string.IsNullOrEmpty(additionalLocation))
  377. {
  378. result.Add(additionalLocation);
  379. }
  380. return result.ToList<string>();
  381. }
  382. }
  383. [SerializeField]
  384. private bool launchAfterInstallation = true;
  385. public bool LaunchAfterInstallation
  386. {
  387. get { return launchAfterInstallation; }
  388. set { launchAfterInstallation = value; }
  389. }
  390. [SerializeField]
  391. private bool HideSendToUnityMenu = true;
  392. public bool HideSendToUnityMenuProperty
  393. {
  394. get { return HideSendToUnityMenu; }
  395. set { HideSendToUnityMenu = value; }
  396. }
  397. [SerializeField]
  398. private bool BakeAnimation = true;
  399. internal bool BakeAnimationProperty
  400. {
  401. get { return BakeAnimation; }
  402. set { BakeAnimation = value; }
  403. }
  404. [SerializeField]
  405. private bool showConvertToPrefabDialog = true;
  406. public bool ShowConvertToPrefabDialog
  407. {
  408. get { return showConvertToPrefabDialog; }
  409. set { showConvertToPrefabDialog = value; }
  410. }
  411. [SerializeField]
  412. private string integrationSavePath;
  413. internal static string IntegrationSavePath
  414. {
  415. get
  416. {
  417. //If the save path gets messed up and ends up not being valid, just use the project folder as the default
  418. if (string.IsNullOrEmpty(instance.integrationSavePath) ||
  419. !Directory.Exists(instance.integrationSavePath))
  420. {
  421. //The project folder, above the asset folder
  422. instance.integrationSavePath = DefaultIntegrationSavePath;
  423. }
  424. return instance.integrationSavePath;
  425. }
  426. set
  427. {
  428. instance.integrationSavePath = value;
  429. }
  430. }
  431. [SerializeField]
  432. private int selectedDCCApp = 0;
  433. internal int SelectedDCCApp
  434. {
  435. get { return selectedDCCApp; }
  436. set { selectedDCCApp = value; }
  437. }
  438. /// <summary>
  439. /// The path where Convert To Model will save the new fbx and prefab.
  440. ///
  441. /// To help teams work together, this is stored to be relative to the
  442. /// Application.dataPath, and the path separator is the forward-slash
  443. /// (e.g. unix and http, not windows).
  444. ///
  445. /// Use GetRelativeSavePath / SetRelativeSavePath to get/set this
  446. /// value, properly interpreted for the current platform.
  447. /// </summary>
  448. [SerializeField]
  449. private List<string> prefabSavePaths = new List<string> ();
  450. [SerializeField]
  451. private List<string> fbxSavePaths = new List<string> ();
  452. [SerializeField]
  453. private int selectedFbxPath = 0;
  454. public int SelectedFbxPath
  455. {
  456. get { return selectedFbxPath; }
  457. set { selectedFbxPath = value; }
  458. }
  459. [SerializeField]
  460. private int selectedPrefabPath = 0;
  461. public int SelectedPrefabPath
  462. {
  463. get { return selectedPrefabPath; }
  464. set { selectedPrefabPath = value; }
  465. }
  466. private int maxStoredSavePaths = 5;
  467. // List of names in order that they appear in option list
  468. [SerializeField]
  469. private List<string> dccOptionNames = new List<string>();
  470. // List of paths in order that they appear in the option list
  471. [SerializeField]
  472. private List<string> dccOptionPaths;
  473. // don't serialize as ScriptableObject does not get properly serialized on export
  474. [System.NonSerialized]
  475. private ExportModelSettings m_exportModelSettings;
  476. internal ExportModelSettings ExportModelSettings
  477. {
  478. get { return m_exportModelSettings; }
  479. set { m_exportModelSettings = value; }
  480. }
  481. // store contents of export model settings for serialization
  482. [SerializeField]
  483. private ExportModelSettingsSerialize exportModelSettingsSerialize;
  484. [System.NonSerialized]
  485. private ConvertToPrefabSettings m_convertToPrefabSettings;
  486. internal ConvertToPrefabSettings ConvertToPrefabSettings
  487. {
  488. get { return m_convertToPrefabSettings; }
  489. set { m_convertToPrefabSettings = value; }
  490. }
  491. [SerializeField]
  492. private ConvertToPrefabSettingsSerialize convertToPrefabSettingsSerialize;
  493. internal override void LoadDefaults()
  494. {
  495. LaunchAfterInstallation = true;
  496. HideSendToUnityMenuProperty = true;
  497. prefabSavePaths = new List<string>(){ kDefaultSavePath };
  498. fbxSavePaths = new List<string> (){ kDefaultSavePath };
  499. integrationSavePath = DefaultIntegrationSavePath;
  500. dccOptionPaths = null;
  501. dccOptionNames = null;
  502. BakeAnimationProperty = true;
  503. ExportModelSettings = ScriptableObject.CreateInstance (typeof(ExportModelSettings)) as ExportModelSettings;
  504. exportModelSettingsSerialize = ExportModelSettings.info;
  505. ShowConvertToPrefabDialog = true;
  506. ConvertToPrefabSettings = ScriptableObject.CreateInstance (typeof(ConvertToPrefabSettings)) as ConvertToPrefabSettings;
  507. convertToPrefabSettingsSerialize = ConvertToPrefabSettings.info;
  508. }
  509. /// <summary>
  510. /// Increments the name if there is a duplicate in dccAppOptions.
  511. /// </summary>
  512. /// <returns>The unique name.</returns>
  513. /// <param name="name">Name.</param>
  514. internal static string GetUniqueDCCOptionName(string name){
  515. Debug.Assert(instance != null);
  516. if (name == null)
  517. {
  518. return null;
  519. }
  520. if (!instance.dccOptionNames.Contains(name)) {
  521. return name;
  522. }
  523. var format = "{1} ({0})";
  524. int index = 1;
  525. // try extracting the current index from the name and incrementing it
  526. var result = System.Text.RegularExpressions.Regex.Match(name, @"\((?<number>\d+?)\)$");
  527. if (result != null) {
  528. var number = result.Groups["number"].Value;
  529. int tempIndex;
  530. if (int.TryParse (number, out tempIndex)) {
  531. var indexOfNumber = name.LastIndexOf (number);
  532. format = name.Remove (indexOfNumber, number.Length).Insert (indexOfNumber, "{0}");
  533. index = tempIndex+1;
  534. }
  535. }
  536. string uniqueName = null;
  537. do {
  538. uniqueName = string.Format (format, index, name);
  539. index++;
  540. } while (instance.dccOptionNames.Contains(uniqueName));
  541. return uniqueName;
  542. }
  543. internal void SetDCCOptionNames(List<string> newList)
  544. {
  545. dccOptionNames = newList;
  546. }
  547. internal void SetDCCOptionPaths(List<string> newList)
  548. {
  549. dccOptionPaths = newList;
  550. }
  551. internal void ClearDCCOptionNames()
  552. {
  553. dccOptionNames.Clear();
  554. }
  555. internal void ClearDCCOptions()
  556. {
  557. SetDCCOptionNames(null);
  558. SetDCCOptionPaths(null);
  559. }
  560. /// <summary>
  561. ///
  562. /// Find the latest program available and make that the default choice.
  563. /// Will always take any Maya version over any 3ds Max version.
  564. ///
  565. /// Returns the index of the most recent program in the list of dccOptionNames
  566. /// Returns -1 on error.
  567. /// </summary>
  568. internal int PreferredDCCApp
  569. {
  570. get
  571. {
  572. if (dccOptionNames == null)
  573. {
  574. return -1;
  575. }
  576. int result = -1;
  577. int newestDCCVersionNumber = -1;
  578. for (int i = 0; i < dccOptionNames.Count; i++)
  579. {
  580. int versionToCheck = FindDCCVersion(dccOptionNames[i]);
  581. if (versionToCheck == -1)
  582. {
  583. if (dccOptionNames[i] == "MAYA_LOCATION")
  584. return i;
  585. continue;
  586. }
  587. if (versionToCheck > newestDCCVersionNumber)
  588. {
  589. result = i;
  590. newestDCCVersionNumber = versionToCheck;
  591. }
  592. else if (versionToCheck == newestDCCVersionNumber)
  593. {
  594. int selection = ChoosePreferredDCCApp(result, i);
  595. if (selection == i)
  596. {
  597. result = i;
  598. newestDCCVersionNumber = FindDCCVersion(dccOptionNames[i]);
  599. }
  600. }
  601. }
  602. return result;
  603. }
  604. }
  605. /// <summary>
  606. /// Takes the index of two program names from dccOptionNames and chooses our preferred one based on the preference list
  607. /// This happens in case of a tie between two programs with the same release year / version
  608. /// </summary>
  609. /// <param name="optionA"></param>
  610. /// <param name="optionB"></param>
  611. /// <returns></returns>
  612. private int ChoosePreferredDCCApp(int optionA, int optionB)
  613. {
  614. Debug.Assert(optionA >= 0 && optionB >= 0 && optionA < dccOptionNames.Count && optionB < dccOptionNames.Count);
  615. if (dccOptionNames.Count == 0)
  616. {
  617. return -1;
  618. }
  619. var appA = dccOptionNames[optionA];
  620. var appB = dccOptionNames[optionB];
  621. if (appA == null || appB == null || appA.Length <= 0 || appB.Length <= 0)
  622. {
  623. return -1;
  624. }
  625. int scoreA = s_PreferenceList.FindIndex(app => RemoveSpacesAndNumbers(app).Equals(RemoveSpacesAndNumbers(appA)));
  626. int scoreB = s_PreferenceList.FindIndex(app => RemoveSpacesAndNumbers(app).Equals(RemoveSpacesAndNumbers(appB)));
  627. return scoreA < scoreB ? optionA : optionB;
  628. }
  629. /// <summary>
  630. /// Takes a given string and removes any spaces or numbers from it
  631. /// </summary>
  632. /// <param name="s"></param>
  633. internal static string RemoveSpacesAndNumbers(string s)
  634. {
  635. return System.Text.RegularExpressions.Regex.Replace(s, @"[\s^0-9]", "");
  636. }
  637. /// <summary>
  638. /// Finds the version based off of the title of the application
  639. /// </summary>
  640. /// <param name="path"></param>
  641. /// <returns> the year/version OR -1 if the year could not be parsed </returns>
  642. private static int FindDCCVersion(string AppName)
  643. {
  644. if (string.IsNullOrEmpty(AppName))
  645. {
  646. return -1;
  647. }
  648. AppName = AppName.Trim();
  649. if (string.IsNullOrEmpty(AppName))
  650. return -1;
  651. string[] piecesArray = AppName.Split(' ');
  652. if (piecesArray.Length < 2)
  653. {
  654. return -1;
  655. }
  656. //Get the number, which is always the last chunk separated by a space.
  657. string number = piecesArray[piecesArray.Length - 1];
  658. int version;
  659. if (int.TryParse(number, out version))
  660. {
  661. return version;
  662. }
  663. else
  664. {
  665. //remove any letters in the string in a final attempt to extract an int from it (this will happen with MayaLT, for example)
  666. string AppNameCopy = AppName;
  667. string stringWithoutLetters = System.Text.RegularExpressions.Regex.Replace(AppNameCopy, "[^0-9]", "");
  668. if (int.TryParse(stringWithoutLetters, out version))
  669. {
  670. return version;
  671. }
  672. float fVersion;
  673. //In case we are looking at something with a decimal based version- the int parse will fail so we'll need to parse it as a float.
  674. if (float.TryParse(number, out fVersion))
  675. {
  676. return (int)fVersion;
  677. }
  678. return -1;
  679. }
  680. }
  681. /// <summary>
  682. /// Find Maya and 3DsMax installations at default install path.
  683. /// Add results to given dictionary.
  684. ///
  685. /// If MAYA_LOCATION is set, add this to the list as well.
  686. /// </summary>
  687. [SecurityPermission(SecurityAction.LinkDemand)]
  688. private static void FindDCCInstalls() {
  689. var dccOptionNames = instance.dccOptionNames;
  690. var dccOptionPaths = instance.dccOptionPaths;
  691. // find dcc installation from vendor locations
  692. for (int i = 0; i < DCCVendorLocations.Count; i++)
  693. {
  694. if (!Directory.Exists(DCCVendorLocations[i]))
  695. {
  696. // no autodesk products installed
  697. continue;
  698. }
  699. // List that directory and find the right version:
  700. // either the newest version, or the exact version we wanted.
  701. var adskRoot = new System.IO.DirectoryInfo(DCCVendorLocations[i]);
  702. foreach (var productDir in adskRoot.GetDirectories())
  703. {
  704. var product = productDir.Name;
  705. // Only accept those that start with 'maya' in either case.
  706. if (product.StartsWith ("maya", StringComparison.InvariantCultureIgnoreCase)) {
  707. string version = product.Substring ("maya".Length);
  708. dccOptionPaths.Add (GetMayaExePathFromLocation (productDir.FullName.Replace ("\\", "/")));
  709. dccOptionNames.Add (GetUniqueDCCOptionName(kMayaOptionName + version));
  710. continue;
  711. }
  712. if (product.StartsWith("3ds max", StringComparison.InvariantCultureIgnoreCase))
  713. {
  714. var exePath = string.Format("{0}/{1}", productDir.FullName.Replace("\\", "/"), "3dsmax.exe");
  715. string version = product.Substring("3ds max ".Length);
  716. var maxOptionName = GetUniqueDCCOptionName(kMaxOptionName + version);
  717. if (IsEarlierThanMax2017(maxOptionName))
  718. {
  719. continue;
  720. }
  721. dccOptionPaths.Add(exePath);
  722. dccOptionNames.Add(maxOptionName);
  723. }
  724. }
  725. }
  726. // add extra locations defined by special environment variables
  727. string location = GetMayaLocationFromEnvironmentVariable("MAYA_LOCATION");
  728. if (!string.IsNullOrEmpty(location))
  729. {
  730. dccOptionPaths.Add(GetMayaExePathFromLocation(location));
  731. dccOptionNames.Add("MAYA_LOCATION");
  732. }
  733. instance.SelectedDCCApp = instance.PreferredDCCApp;
  734. }
  735. /// <summary>
  736. /// Returns the first valid folder in our list of vendor locations
  737. /// </summary>
  738. /// <returns>The first valid vendor location</returns>
  739. internal static string FirstValidVendorLocation
  740. {
  741. [SecurityPermission(SecurityAction.LinkDemand)]
  742. get
  743. {
  744. List<string> locations = DCCVendorLocations;
  745. for (int i = 0; i < locations.Count; i++)
  746. {
  747. //Look through the list of locations we have and take the first valid one
  748. if (Directory.Exists(locations[i]))
  749. {
  750. return locations[i];
  751. }
  752. }
  753. //if no valid locations exist, just take us to the project folder
  754. return Directory.GetCurrentDirectory();
  755. }
  756. }
  757. /// <summary>
  758. /// Gets the maya exe at Maya install location.
  759. /// </summary>
  760. /// <returns>The maya exe path.</returns>
  761. /// <param name="location">Location of Maya install.</param>
  762. private static string GetMayaExePathFromLocation(string location)
  763. {
  764. switch (Application.platform) {
  765. case RuntimePlatform.WindowsEditor:
  766. return location + "/bin/maya.exe";
  767. case RuntimePlatform.OSXEditor:
  768. // MAYA_LOCATION on mac is set by Autodesk to be the
  769. // Contents directory. But let's make it easier on people
  770. // and allow just having it be the app bundle or a
  771. // directory that holds the app bundle.
  772. if (location.EndsWith(".app/Contents")) {
  773. return location + "/MacOS/Maya";
  774. } else if (location.EndsWith(".app")) {
  775. return location + "/Contents/MacOS/Maya";
  776. } else {
  777. return location + "/Maya.app/Contents/MacOS/Maya";
  778. }
  779. default:
  780. throw new NotImplementedException ();
  781. }
  782. }
  783. [SecurityPermission(SecurityAction.LinkDemand)]
  784. internal static GUIContent[] GetDCCOptions(){
  785. if (instance.dccOptionNames == null ||
  786. instance.dccOptionNames.Count != instance.dccOptionPaths.Count ||
  787. instance.dccOptionNames.Count == 0) {
  788. instance.dccOptionPaths = new List<string> ();
  789. instance.dccOptionNames = new List<string> ();
  790. FindDCCInstalls ();
  791. }
  792. // store the selected app if any
  793. string prevSelection = SelectedDCCPath;
  794. // remove options that no longer exist
  795. List<string> pathsToDelete = new List<string>();
  796. List<string> namesToDelete = new List<string>();
  797. for(int i = 0; i < instance.dccOptionPaths.Count; i++) {
  798. var dccPath = instance.dccOptionPaths [i];
  799. if (!File.Exists (dccPath)) {
  800. namesToDelete.Add (instance.dccOptionNames [i]);
  801. pathsToDelete.Add (dccPath);
  802. }
  803. }
  804. foreach (var str in pathsToDelete) {
  805. instance.dccOptionPaths.Remove (str);
  806. }
  807. foreach (var str in namesToDelete) {
  808. instance.dccOptionNames.Remove (str);
  809. }
  810. // set the selected DCC app to the previous selection
  811. instance.SelectedDCCApp = instance.dccOptionPaths.IndexOf (prevSelection);
  812. if (instance.SelectedDCCApp < 0) {
  813. // find preferred app if previous selection no longer exists
  814. instance.SelectedDCCApp = instance.PreferredDCCApp;
  815. }
  816. if (instance.dccOptionPaths.Count <= 0) {
  817. instance.SelectedDCCApp = 0;
  818. return new GUIContent[]{
  819. new GUIContent("<No 3D Application found>")
  820. };
  821. }
  822. GUIContent[] optionArray = new GUIContent[instance.dccOptionPaths.Count];
  823. for(int i = 0; i < instance.dccOptionPaths.Count; i++){
  824. optionArray [i] = new GUIContent(
  825. instance.dccOptionNames[i],
  826. instance.dccOptionPaths[i]
  827. );
  828. }
  829. return optionArray;
  830. }
  831. internal enum DCCType { Maya, Max };
  832. [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  833. [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  834. internal static void AddDCCOption(string newOption, DCCType dcc){
  835. if (Application.platform == RuntimePlatform.OSXEditor && dcc == DCCType.Maya) {
  836. // on OSX we get a path ending in .app, which is not quite the exe
  837. newOption = GetMayaExePathFromLocation(newOption);
  838. }
  839. var dccOptionPaths = instance.dccOptionPaths;
  840. if (dccOptionPaths.Contains(newOption)) {
  841. instance.SelectedDCCApp = dccOptionPaths.IndexOf (newOption);
  842. return;
  843. }
  844. string optionName = "";
  845. switch (dcc) {
  846. case DCCType.Maya:
  847. var version = AskMayaVersion(newOption);
  848. if (version == null)
  849. {
  850. Debug.LogError("This version of Maya could not be launched properly");
  851. UnityEditor.EditorUtility.DisplayDialog("Error Loading 3D Application",
  852. "Failed to add Maya option, could not get version number from maya.exe",
  853. "Ok");
  854. return;
  855. }
  856. optionName = GetUniqueDCCOptionName("Maya " + version);
  857. break;
  858. case DCCType.Max:
  859. optionName = GetMaxOptionName (newOption);
  860. if (ExportSettings.IsEarlierThanMax2017(optionName))
  861. {
  862. Debug.LogError("Earlier than 3ds Max 2017 is not supported");
  863. UnityEditor.EditorUtility.DisplayDialog(
  864. "Error adding 3D Application",
  865. "Unity Integration only supports 3ds Max 2017 or later",
  866. "Ok");
  867. return;
  868. }
  869. break;
  870. default:
  871. throw new System.NotImplementedException();
  872. }
  873. instance.dccOptionNames.Add (optionName);
  874. dccOptionPaths.Add (newOption);
  875. instance.SelectedDCCApp = dccOptionPaths.Count - 1;
  876. }
  877. /// <summary>
  878. /// Ask the version number by running maya.
  879. /// </summary>
  880. [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  881. [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
  882. internal static string AskMayaVersion(string exePath) {
  883. System.Diagnostics.Process myProcess = new System.Diagnostics.Process();
  884. myProcess.StartInfo.FileName = exePath;
  885. myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
  886. myProcess.StartInfo.CreateNoWindow = true;
  887. myProcess.StartInfo.UseShellExecute = false;
  888. myProcess.StartInfo.RedirectStandardOutput = true;
  889. myProcess.StartInfo.Arguments = "-v";
  890. myProcess.EnableRaisingEvents = true;
  891. myProcess.Start();
  892. string resultString = myProcess.StandardOutput.ReadToEnd();
  893. myProcess.WaitForExit();
  894. // Output is like: Maya 2018, Cut Number 201706261615
  895. // We want the stuff after 'Maya ' and before the comma.
  896. // (Uni-31601) less brittle! Consider also the mel command "about -version".
  897. if (string.IsNullOrEmpty(resultString))
  898. {
  899. return null;
  900. }
  901. resultString = resultString.Trim();
  902. var commaIndex = resultString.IndexOf(',');
  903. if (commaIndex != -1)
  904. {
  905. const int versionStart = 5; // length of "Maya "
  906. return resultString.Length > versionStart ? resultString.Substring(0, commaIndex).Substring(versionStart) : null;
  907. }
  908. else
  909. {
  910. //This probably means we tried to launch Maya to check the version but it was some sort of broken maya.
  911. //We'll just return null and throw an error for it.
  912. return null;
  913. }
  914. }
  915. /// <summary>
  916. /// Gets the unique label for a new 3DsMax dropdown option.
  917. /// </summary>
  918. /// <returns>The 3DsMax dropdown option label.</returns>
  919. /// <param name="exePath">Exe path.</param>
  920. internal static string GetMaxOptionName(string exePath){
  921. return GetUniqueDCCOptionName(Path.GetFileName(Path.GetDirectoryName (exePath)));
  922. }
  923. internal static bool IsEarlierThanMax2017(string AppName){
  924. int version = FindDCCVersion(AppName);
  925. return version != -1 && version < 2017;
  926. }
  927. internal static string SelectedDCCPath
  928. {
  929. get
  930. {
  931. return (instance.dccOptionPaths.Count > 0 &&
  932. instance.SelectedDCCApp >= 0 &&
  933. instance.SelectedDCCApp < instance.dccOptionPaths.Count) ? instance.dccOptionPaths[instance.SelectedDCCApp] : "";
  934. }
  935. }
  936. internal static string SelectedDCCName
  937. {
  938. get
  939. {
  940. return (instance.dccOptionNames.Count > 0 &&
  941. instance.SelectedDCCApp >= 0 &&
  942. instance.SelectedDCCApp < instance.dccOptionNames.Count) ? instance.dccOptionNames[instance.SelectedDCCApp] : "";
  943. }
  944. }
  945. internal static bool CanInstall()
  946. {
  947. return instance.dccOptionPaths.Count > 0;
  948. }
  949. internal static string GetProjectRelativePath(string fullPath){
  950. var assetRelativePath = UnityEditor.Formats.Fbx.Exporter.ExportSettings.ConvertToAssetRelativePath(fullPath);
  951. var projectRelativePath = "Assets/" + assetRelativePath;
  952. if (string.IsNullOrEmpty(assetRelativePath)) {
  953. throw new FbxExportSettingsException("Path " + fullPath + " must be in the Assets folder.");
  954. }
  955. return projectRelativePath;
  956. }
  957. /// <summary>
  958. /// The relative save paths for given absolute paths.
  959. /// This is relative to the Application.dataPath ; it uses '/' as the
  960. /// separator on all platforms.
  961. /// </summary>
  962. internal static string[] GetRelativeSavePaths(List<string> exportSavePaths){
  963. if(exportSavePaths == null)
  964. {
  965. return null;
  966. }
  967. if (exportSavePaths.Count == 0) {
  968. exportSavePaths.Add (kDefaultSavePath);
  969. }
  970. string[] relSavePaths = new string[exportSavePaths.Count];
  971. // use special forward slash unicode char as "/" is a special character
  972. // that affects the dropdown layout.
  973. string forwardslash = " \u2044 ";
  974. for (int i = 0; i < relSavePaths.Length; i++) {
  975. relSavePaths [i] = string.Format("Assets{0}{1}", forwardslash, exportSavePaths[i] == "."? "" : NormalizePath(exportSavePaths [i], isRelative: true).Replace("/", forwardslash));
  976. }
  977. return relSavePaths;
  978. }
  979. /// <summary>
  980. /// Returns the paths for display in the menu.
  981. /// Paths inside the Assets folder are relative, while those outside are kept absolute.
  982. /// </summary>
  983. internal static string[] GetMixedSavePaths(List<string> exportSavePaths)
  984. {
  985. string[] displayPaths = new string[exportSavePaths.Count];
  986. string forwardslash = " \u2044 ";
  987. for (int i = 0; i < displayPaths.Length; i++)
  988. {
  989. // if path is in Assets folder, shorten it
  990. if (!Path.IsPathRooted(exportSavePaths[i]))
  991. {
  992. displayPaths[i] = string.Format("Assets{0}{1}", forwardslash, exportSavePaths[i] == "."? "" : NormalizePath(exportSavePaths [i], isRelative: true).Replace("/", forwardslash));
  993. }
  994. else
  995. {
  996. displayPaths[i] = exportSavePaths[i].Replace("/", forwardslash);
  997. }
  998. }
  999. return displayPaths;
  1000. }
  1001. /// <summary>
  1002. /// The path where Export model will save the new fbx.
  1003. /// This is relative to the Application.dataPath ; it uses '/' as the
  1004. /// separator on all platforms.
  1005. /// Only returns the paths within the Assets folder of the project.
  1006. /// </summary>
  1007. internal static string[] GetRelativeFbxSavePaths(){
  1008. // sort the list of paths, putting project paths first
  1009. instance.fbxSavePaths.Sort((x, y) => Path.IsPathRooted(x).CompareTo(Path.IsPathRooted(y)));
  1010. var relPathCount = instance.fbxSavePaths.FindAll(x => !Path.IsPathRooted(x)).Count;
  1011. // reset selected path if it's out of range
  1012. if (instance.SelectedFbxPath > relPathCount - 1)
  1013. {
  1014. instance.SelectedFbxPath = 0;
  1015. }
  1016. return GetRelativeSavePaths(instance.fbxSavePaths.GetRange(0, relPathCount));
  1017. }
  1018. /// <summary>
  1019. /// The path where Convert to Prefab will save the new prefab.
  1020. /// This is relative to the Application.dataPath ; it uses '/' as the
  1021. /// separator on all platforms.
  1022. /// </summary>
  1023. internal static string[] GetRelativePrefabSavePaths(){
  1024. return GetRelativeSavePaths(instance.prefabSavePaths);
  1025. }
  1026. /// <summary>
  1027. /// The paths formatted for display in the menu.
  1028. /// Paths outside the Assets folder are kept as they are and ones inside are shortened.
  1029. /// </summary>
  1030. internal static string[] GetMixedFbxSavePaths()
  1031. {
  1032. return GetMixedSavePaths(instance.fbxSavePaths);
  1033. }
  1034. /// <summary>
  1035. /// Adds the save path to given save path list.
  1036. /// </summary>
  1037. /// <param name="savePath">Save path.</param>
  1038. /// <param name="exportSavePaths">Export save paths.</param>
  1039. private static void AddSavePath(string savePath, ref List<string> exportSavePaths){
  1040. if(exportSavePaths == null)
  1041. {
  1042. return;
  1043. }
  1044. if (ExportSettings.instance.ExportOutsideProject)
  1045. {
  1046. savePath = NormalizePath(savePath, isRelative: false);
  1047. }
  1048. else
  1049. {
  1050. savePath = NormalizePath(savePath, isRelative: true);
  1051. }
  1052. if (exportSavePaths.Contains (savePath)) {
  1053. // move to first place if it isn't already
  1054. if (exportSavePaths [0] == savePath) {
  1055. return;
  1056. }
  1057. exportSavePaths.Remove (savePath);
  1058. }
  1059. if (exportSavePaths.Count >= instance.maxStoredSavePaths) {
  1060. // remove last used path
  1061. exportSavePaths.RemoveAt(exportSavePaths.Count-1);
  1062. }
  1063. exportSavePaths.Insert (0, savePath);
  1064. }
  1065. internal static void AddFbxSavePath(string savePath){
  1066. AddSavePath (savePath, ref instance.fbxSavePaths);
  1067. instance.SelectedFbxPath = 0;
  1068. }
  1069. internal static void AddPrefabSavePath(string savePath){
  1070. AddSavePath (savePath, ref instance.prefabSavePaths);
  1071. instance.SelectedPrefabPath = 0;
  1072. }
  1073. internal static string GetAbsoluteSavePath(string savePath){
  1074. var projectAbsolutePath = Path.Combine(Application.dataPath, savePath);
  1075. projectAbsolutePath = NormalizePath(projectAbsolutePath, isRelative: false, separator: Path.DirectorySeparatorChar);
  1076. // if path is outside Assets folder, it's already absolute so return the original path
  1077. if (string.IsNullOrEmpty(ExportSettings.ConvertToAssetRelativePath(projectAbsolutePath)))
  1078. {
  1079. return savePath;
  1080. }
  1081. return projectAbsolutePath;
  1082. }
  1083. internal static string FbxAbsoluteSavePath{
  1084. get
  1085. {
  1086. if (instance.fbxSavePaths.Count <= 0)
  1087. {
  1088. instance.fbxSavePaths.Add(kDefaultSavePath);
  1089. }
  1090. return GetAbsoluteSavePath(instance.fbxSavePaths[instance.SelectedFbxPath]);
  1091. }
  1092. }
  1093. internal static string PrefabAbsoluteSavePath{
  1094. get
  1095. {
  1096. if (instance.prefabSavePaths.Count <= 0)
  1097. {
  1098. instance.prefabSavePaths.Add(kDefaultSavePath);
  1099. }
  1100. return GetAbsoluteSavePath(instance.prefabSavePaths[instance.SelectedPrefabPath]);
  1101. }
  1102. }
  1103. /// <summary>
  1104. /// Convert an absolute path into a relative path like what you would
  1105. /// get from GetRelativeSavePath.
  1106. ///
  1107. /// This uses '/' as the path separator.
  1108. ///
  1109. /// If 'requireSubdirectory' is the default on, return empty-string if the full
  1110. /// path is not in a subdirectory of assets.
  1111. /// </summary>
  1112. internal static string ConvertToAssetRelativePath(string fullPathInAssets, bool requireSubdirectory = true)
  1113. {
  1114. if (!Path.IsPathRooted(fullPathInAssets)) {
  1115. fullPathInAssets = Path.GetFullPath(fullPathInAssets);
  1116. }
  1117. var relativePath = GetRelativePath(Application.dataPath, fullPathInAssets);
  1118. if (requireSubdirectory && relativePath.StartsWith("..")) {
  1119. if (relativePath.Length == 2 || relativePath[2] == '/') {
  1120. // The relative path has us pop out to another directory,
  1121. // so return an empty string as requested.
  1122. return "";
  1123. }
  1124. }
  1125. return relativePath;
  1126. }
  1127. /// <summary>
  1128. /// Compute how to get from 'fromDir' to 'toDir' via a relative path.
  1129. /// </summary>
  1130. internal static string GetRelativePath(string fromDir, string toDir,
  1131. char separator = '/')
  1132. {
  1133. // https://stackoverflow.com/questions/275689/how-to-get-relative-path-from-absolute-path
  1134. // Except... the MakeRelativeUri that ships with Unity is buggy.
  1135. // e.g. https://bugzilla.xamarin.com/show_bug.cgi?id=5921
  1136. // among other bugs. So we roll our own.
  1137. // Normalize the paths, assuming they're absolute paths (if they
  1138. // aren't, they get normalized as relative paths)
  1139. fromDir = NormalizePath(fromDir, isRelative: false);
  1140. toDir = NormalizePath(toDir, isRelative: false);
  1141. // Break them into path components.
  1142. var fromDirs = fromDir.Split('/');
  1143. var toDirs = toDir.Split('/');
  1144. // Find the least common ancestor
  1145. int lca = -1;
  1146. for(int i = 0, n = System.Math.Min(fromDirs.Length, toDirs.Length); i < n; ++i) {
  1147. if (fromDirs[i] != toDirs[i]) { break; }
  1148. lca = i;
  1149. }
  1150. // Step up from the fromDir to the lca, then down from lca to the toDir.
  1151. // If from = /a/b/c/d
  1152. // and to = /a/b/e/f/g
  1153. // Then we need to go up 2 and down 3.
  1154. var nStepsUp = (fromDirs.Length - 1) - lca;
  1155. var nStepsDown = (toDirs.Length - 1) - lca;
  1156. if (nStepsUp + nStepsDown == 0) {
  1157. return ".";
  1158. }
  1159. var relDirs = new string[nStepsUp + nStepsDown];
  1160. for(int i = 0; i < nStepsUp; ++i) {
  1161. relDirs[i] = "..";
  1162. }
  1163. for(int i = 0; i < nStepsDown; ++i) {
  1164. relDirs[nStepsUp + i] = toDirs[lca + 1 + i];
  1165. }
  1166. return string.Join("" + separator, relDirs);
  1167. }
  1168. /// <summary>
  1169. /// Normalize a path, cleaning up path separators, resolving '.' and
  1170. /// '..', removing duplicate and trailing path separators, etc.
  1171. ///
  1172. /// If the path passed in is a relative path, we remove leading path separators.
  1173. /// If it's an absolute path we don't.
  1174. ///
  1175. /// If you claim the path is absolute but actually it's relative, we
  1176. /// treat it as a relative path.
  1177. /// </summary>
  1178. internal static string NormalizePath(string path, bool isRelative,
  1179. char separator = '/')
  1180. {
  1181. if(path == null)
  1182. {
  1183. return null;
  1184. }
  1185. // Use slashes to simplify the code (we're going to clobber them all anyway).
  1186. path = path.Replace('\\', '/');
  1187. // If we're supposed to be an absolute path, but we're actually a
  1188. // relative path, ignore the 'isRelative' flag.
  1189. if (!isRelative && !Path.IsPathRooted(path)) {
  1190. isRelative = true;
  1191. }
  1192. // Build up a list of directory items.
  1193. var dirs = path.Split('/');
  1194. // Modify dirs in-place, reading from readIndex and remembering
  1195. // what index we've written to.
  1196. int lastWriteIndex = -1;
  1197. for (int readIndex = 0, n = dirs.Length; readIndex < n; ++readIndex) {
  1198. var dir = dirs[readIndex];
  1199. // Skip duplicate path separators.
  1200. if (string.IsNullOrEmpty(dir)) {
  1201. // Skip if it's not a leading path separator.
  1202. if (lastWriteIndex >= 0) {
  1203. continue; }
  1204. // Also skip if it's leading and we have a relative path.
  1205. if (isRelative) {
  1206. continue;
  1207. }
  1208. }
  1209. // Skip '.'
  1210. if (dir == ".") {
  1211. continue;
  1212. }
  1213. // Erase the previous directory we read on '..'.
  1214. // Exception: we can start with '..'
  1215. // Exception: we can have multiple '..' in a row.
  1216. //
  1217. // Note: this ignores the actual file system and the funny
  1218. // results you see when there are symlinks.
  1219. if (dir == "..") {
  1220. if (lastWriteIndex == -1) {
  1221. // Leading '..' => handle like a normal directory.
  1222. } else if (dirs[lastWriteIndex] == "..") {
  1223. // Multiple ".." => handle like a normal directory.
  1224. } else {
  1225. // Usual case: delete the previous directory.
  1226. lastWriteIndex--;
  1227. continue;
  1228. }
  1229. }
  1230. // Copy anything else to the next index.
  1231. ++lastWriteIndex;
  1232. dirs[lastWriteIndex] = dirs[readIndex];
  1233. }
  1234. if (lastWriteIndex == -1 || (lastWriteIndex == 0 && string.IsNullOrEmpty(dirs[lastWriteIndex]))) {
  1235. // If we didn't keep anything, we have the empty path.
  1236. // For an absolute path that's / ; for a relative path it's .
  1237. if (isRelative) {
  1238. return ".";
  1239. } else {
  1240. return "" + separator;
  1241. }
  1242. } else {
  1243. // Otherwise print out the path with the proper separator.
  1244. return String.Join("" + separator, dirs, 0, lastWriteIndex + 1);
  1245. }
  1246. }
  1247. internal override void Load ()
  1248. {
  1249. base.Load ();
  1250. if (!instance.ExportModelSettings) {
  1251. instance.ExportModelSettings = ScriptableObject.CreateInstance (typeof(ExportModelSettings)) as ExportModelSettings;
  1252. }
  1253. instance.ExportModelSettings.info = instance.exportModelSettingsSerialize;
  1254. if (!instance.ConvertToPrefabSettings) {
  1255. instance.ConvertToPrefabSettings = ScriptableObject.CreateInstance (typeof(ConvertToPrefabSettings)) as ConvertToPrefabSettings;
  1256. }
  1257. instance.ConvertToPrefabSettings.info = instance.convertToPrefabSettingsSerialize;
  1258. }
  1259. internal void Save()
  1260. {
  1261. exportModelSettingsSerialize = ExportModelSettings.info;
  1262. convertToPrefabSettingsSerialize = ConvertToPrefabSettings.info;
  1263. instance.Save (true);
  1264. }
  1265. }
  1266. internal abstract class ScriptableSingleton<T> : ScriptableObject where T : ScriptableSingleton<T>
  1267. {
  1268. private static T s_Instance;
  1269. public static T instance
  1270. {
  1271. get
  1272. {
  1273. if (s_Instance == null)
  1274. {
  1275. s_Instance = ScriptableObject.CreateInstance<T>();
  1276. s_Instance.Load();
  1277. }
  1278. return s_Instance;
  1279. }
  1280. }
  1281. internal ScriptableSingleton()
  1282. {
  1283. if (s_Instance != null)
  1284. {
  1285. Debug.LogError(typeof(T) + " already exists. Did you query the singleton in a constructor?");
  1286. }
  1287. }
  1288. internal abstract void LoadDefaults();
  1289. internal virtual void Load()
  1290. {
  1291. string filePath = GetFilePath();
  1292. if (!System.IO.File.Exists(filePath)) {
  1293. LoadDefaults();
  1294. } else {
  1295. try {
  1296. var fileData = System.IO.File.ReadAllText(filePath);
  1297. EditorJsonUtility.FromJsonOverwrite(fileData, s_Instance);
  1298. } catch(Exception xcp) {
  1299. // Quash the exception and take the default settings.
  1300. Debug.LogException(xcp);
  1301. LoadDefaults();
  1302. }
  1303. }
  1304. }
  1305. internal virtual void Save(bool saveAsText)
  1306. {
  1307. if (s_Instance == null)
  1308. {
  1309. Debug.Log("Cannot save ScriptableSingleton: no instance!");
  1310. return;
  1311. }
  1312. string filePath = GetFilePath();
  1313. if (!string.IsNullOrEmpty(filePath))
  1314. {
  1315. string directoryName = Path.GetDirectoryName(filePath);
  1316. if (!Directory.Exists(directoryName))
  1317. {
  1318. Directory.CreateDirectory(directoryName);
  1319. }
  1320. System.IO.File.WriteAllText(filePath, EditorJsonUtility.ToJson(s_Instance, true));
  1321. }
  1322. }
  1323. private static string GetFilePath()
  1324. {
  1325. foreach(var attr in typeof(T).GetCustomAttributes(true)) {
  1326. FilePathAttribute filePathAttribute = attr as FilePathAttribute;
  1327. if (filePathAttribute != null)
  1328. {
  1329. return filePathAttribute.filepath;
  1330. }
  1331. }
  1332. return null;
  1333. }
  1334. }
  1335. [AttributeUsage(AttributeTargets.Class)]
  1336. internal sealed class FilePathAttribute : Attribute
  1337. {
  1338. public enum Location
  1339. {
  1340. PreferencesFolder,
  1341. ProjectFolder
  1342. }
  1343. public string filepath
  1344. {
  1345. get;
  1346. set;
  1347. }
  1348. public FilePathAttribute(string relativePath, FilePathAttribute.Location location)
  1349. {
  1350. if (string.IsNullOrEmpty(relativePath))
  1351. {
  1352. Debug.LogError("Invalid relative path! (its null or empty)");
  1353. return;
  1354. }
  1355. if (relativePath[0] == '/')
  1356. {
  1357. relativePath = relativePath.Substring(1);
  1358. }
  1359. if (location == FilePathAttribute.Location.PreferencesFolder)
  1360. {
  1361. this.filepath = InternalEditorUtility.unityPreferencesFolder + "/" + relativePath;
  1362. }
  1363. else
  1364. {
  1365. this.filepath = relativePath;
  1366. }
  1367. }
  1368. }
  1369. }