using System; using System.IO; using UnityEditorInternal; using UnityEngine; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using UnityEditor.Presets; namespace UnityEditor.Formats.Fbx.Exporter { /// /// FBX export format options. /// public enum ExportFormat { /// /// Output the FBX in ASCII format. /// ASCII = 0, /// /// Output the FBX in Binary format. /// Binary = 1 } /// /// Options for the type of data to include in the export /// (Model only, animation only, or model and animation). /// public enum Include { /// /// Export the model without animation. /// Model = 0, /// /// Export the animation only. /// Anim = 1, /// /// Export both the model and animation. /// ModelAndAnim = 2 } /// /// Options for the position to use for the root GameObject. /// public enum ObjectPosition { /// /// For a single root, uses the local transform information. /// If you select multiple GameObjects for export, the FBX Exporter centers GameObjects /// around a shared root while keeping their relative placement unchanged. /// LocalCentered = 0, /// /// Uses the world position of the GameObjects. /// WorldAbsolute = 1, /// /// Exports the GameObject to (0,0,0). /// For convert to FBX prefab variant only, no UI option. /// Reset = 2 } /// /// LODs to export for LOD groups. /// /// /// Notes: /// - The FBX Exporter ignores LODs outside of selected hierarchy. /// - The FBX Exporter does not filter out objects that are used as LODs and doesn't /// export them if they aren’t direct descendants of their respective LOD Group /// public enum LODExportType { /// /// Export all LODs. /// All = 0, /// /// Export only the highest LOD. /// Highest = 1, /// /// Export only the lowest LOD. /// Lowest = 2 } /// /// Exception class for FBX export settings. /// [System.Serializable] public class FbxExportSettingsException : System.Exception { internal FbxExportSettingsException() {} internal FbxExportSettingsException(string message) : base(message) {} internal FbxExportSettingsException(string message, System.Exception inner) : base(message, inner) {} internal FbxExportSettingsException(SerializationInfo info, StreamingContext context) : base(info, context) {} } [CustomEditor(typeof(ExportSettings))] internal class ExportSettingsEditor : UnityEditor.Editor { Vector2 scrollPos = Vector2.zero; const float LabelWidth = 180; const float SelectableLabelMinWidth = 90; const float BrowseButtonWidth = 25; const float FieldOffset = 18; const float BrowseButtonOffset = 5; const float ExportOptionsLabelWidth = 205; const float ExportOptionsFieldOffset = 18; private bool m_showExportSettingsOptions = true; private bool m_showConvertSettingsOptions = true; private ExportModelSettingsEditor m_exportModelEditor; private ConvertToPrefabSettingsEditor m_convertEditor; private void OnEnable() { ExportSettings exportSettings = (ExportSettings)target; m_exportModelEditor = UnityEditor.Editor.CreateEditor(exportSettings.ExportModelSettings) as ExportModelSettingsEditor; m_convertEditor = UnityEditor.Editor.CreateEditor(exportSettings.ConvertToPrefabSettings) as ConvertToPrefabSettingsEditor; } static class Style { public static GUIContent Application3D = new GUIContent( "3D Application", "Select the 3D Application for which you would like to install the Unity integration."); public static GUIContent KeepOpen = new GUIContent("Keep Open", "Keep the selected 3D application open after Unity integration install has completed."); public static GUIContent HideNativeMenu = new GUIContent("Hide Native Menu", "Replace Maya's native 'Send to Unity' menu with the Unity Integration's menu"); public static GUIContent InstallIntegrationContent = new GUIContent( "Install Unity Integration", "Install and configure the Unity integration for the selected 3D application so that you can import and export directly with this project."); public static GUIContent RepairMissingScripts = new GUIContent( "Run Component Updater", "If FBX exporter version 1.3.0f1 or earlier was previously installed, then links to the FbxPrefab component will need updating.\n" + "Run this to update all FbxPrefab references in text serialized prefabs and scene files."); public static GUIContent DisplayOptionsWindow = new GUIContent( "Display Options Window", "Show the Convert dialog when converting to an FBX Prefab Variant"); } private void ClearExportWindowSettings(string prefix) where T : ExportOptionsEditorWindow { string defaultSettings = null; if (typeof(T) == typeof(ExportModelEditorWindow)) { defaultSettings = EditorJsonUtility.ToJson(ExportSettings.instance.ExportModelSettings.info); } else { defaultSettings = EditorJsonUtility.ToJson(ExportSettings.instance.ConvertToPrefabSettings.info); } if (EditorWindow.HasOpenInstances()) { // clear settings on the window and update the UI var win = EditorWindow.GetWindow(ExportOptionsEditorWindow.DefaultWindowTitle, focus: false); win.ResetSessionSettings(defaultSettings); win.Repaint(); } else { // Clear what is stored in the session. // Window will update next time it is opened ExportOptionsEditorWindow.ResetAllSessionSettings(prefix, defaultSettings); } } private void ShowExportPathUI(string label, string tooltip, string openFolderPanelTitle, bool isSingletonInstance, bool isConvertToPrefabOptions = false) { GUILayout.BeginHorizontal(); EditorGUILayout.LabelField(new GUIContent(label, tooltip), GUILayout.Width(ExportOptionsLabelWidth - ExportOptionsFieldOffset)); var pathLabels = ExportSettings.GetMixedFbxSavePaths(); if (isConvertToPrefabOptions) { pathLabels = ExportSettings.GetRelativePrefabSavePaths(); } EditorGUI.BeginChangeCheck(); if (isConvertToPrefabOptions) { ExportSettings.instance.SelectedPrefabPath = EditorGUILayout.Popup(ExportSettings.instance.SelectedPrefabPath, pathLabels, GUILayout.MinWidth(SelectableLabelMinWidth)); } else { ExportSettings.instance.SelectedFbxPath = EditorGUILayout.Popup(ExportSettings.instance.SelectedFbxPath, pathLabels, GUILayout.MinWidth(SelectableLabelMinWidth)); } if (EditorGUI.EndChangeCheck() && isSingletonInstance) { if (isConvertToPrefabOptions) { ClearExportWindowSettings(ConvertToPrefabEditorWindow.k_SessionStoragePrefix); } else { ClearExportWindowSettings(ExportModelEditorWindow.k_SessionStoragePrefix); } } // Set export setting for exporting outside the project on choosing a path if (!isConvertToPrefabOptions) { // Set export setting for exporting outside the project on choosing a path var exportOutsideProject = !pathLabels[ExportSettings.instance.SelectedFbxPath].Substring(0, 6).Equals("Assets"); m_exportModelEditor.SetExportingOutsideProject(exportOutsideProject); } EditorGUI.BeginDisabledGroup(!isSingletonInstance); var str = isConvertToPrefabOptions ? "save prefab" : "export"; var buttonTooltip = string.Format("Browse to a new location to {0} to", str); if (GUILayout.Button(new GUIContent("...", buttonTooltip), EditorStyles.miniButton, GUILayout.Width(BrowseButtonWidth))) { string initialPath = Application.dataPath; string fullPath = EditorUtility.SaveFolderPanel( openFolderPanelTitle, initialPath, null ); // Unless the user canceled, save path. if (!string.IsNullOrEmpty(fullPath)) { var relativePath = ExportSettings.ConvertToAssetRelativePath(fullPath); // We're exporting outside Assets folder, so store the absolute path if (string.IsNullOrEmpty(relativePath)) { if (isConvertToPrefabOptions) { Debug.LogWarning("Please select a location in the Assets folder"); } else { ExportSettings.AddFbxSavePath(fullPath, exportOutsideProject: true); ClearExportWindowSettings(ExportModelEditorWindow.k_SessionStoragePrefix); } } // Store the relative path to the Assets folder else { if (isConvertToPrefabOptions) { ExportSettings.AddPrefabSavePath(relativePath); ClearExportWindowSettings(ConvertToPrefabEditorWindow.k_SessionStoragePrefix); } else { ExportSettings.AddFbxSavePath(relativePath); ClearExportWindowSettings(ExportModelEditorWindow.k_SessionStoragePrefix); } } // Make sure focus is removed from the selectable label // otherwise it won't update GUIUtility.hotControl = 0; GUIUtility.keyboardControl = 0; } } EditorGUI.EndDisabledGroup(); GUILayout.EndHorizontal(); } public override void OnInspectorGUI() { ExportSettings exportSettings = (ExportSettings)target; bool isSingletonInstance = this.targets.Length == 1 && this.target == ExportSettings.instance; // Increasing the label width so that none of the text gets cut off EditorGUIUtility.labelWidth = LabelWidth; scrollPos = GUILayout.BeginScrollView(scrollPos); var version = UnityEditor.Formats.Fbx.Exporter.ModelExporter.GetVersionFromReadme(); if (!string.IsNullOrEmpty(version)) { GUILayout.Label("Version: " + version, EditorStyles.centeredGreyMiniLabel); EditorGUILayout.Space(); } GUILayout.BeginVertical(); EditorGUILayout.LabelField("Export Options", EditorStyles.boldLabel); EditorGUI.indentLevel++; EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(Style.DisplayOptionsWindow, GUILayout.Width(LabelWidth)); exportSettings.DisplayOptionsWindow = EditorGUILayout.Toggle( exportSettings.DisplayOptionsWindow ); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); // EXPORT SETTINGS m_showExportSettingsOptions = EditorGUILayout.Foldout(m_showExportSettingsOptions, "FBX File Options", EditorStyles.foldoutHeader); if (m_showExportSettingsOptions) { EditorGUI.indentLevel++; ShowExportPathUI("Export Path", "Location where the FBX will be saved.", "Select Export Model Path", isSingletonInstance, isConvertToPrefabOptions: false); EditorGUI.BeginChangeCheck(); m_exportModelEditor.LabelWidth = ExportOptionsLabelWidth; m_exportModelEditor.FieldOffset = ExportOptionsFieldOffset; m_exportModelEditor.OnInspectorGUI(); if (EditorGUI.EndChangeCheck() && isSingletonInstance) { ClearExportWindowSettings(ExportModelEditorWindow.k_SessionStoragePrefix); } EditorGUI.indentLevel--; } // -------------------------- // CONVERT TO PREFAB SETTINGS m_showConvertSettingsOptions = EditorGUILayout.Foldout(m_showConvertSettingsOptions, "Convert to Prefab Options", EditorStyles.foldoutHeader); if (m_showConvertSettingsOptions) { EditorGUI.indentLevel++; ShowExportPathUI("Prefab Path", "Relative path for saving FBX Prefab Variants.", "Select FBX Prefab Variant Save Path", isSingletonInstance, isConvertToPrefabOptions: true); EditorGUI.BeginChangeCheck(); m_convertEditor.LabelWidth = ExportOptionsLabelWidth; m_convertEditor.FieldOffset = ExportOptionsFieldOffset; m_convertEditor.OnInspectorGUI(); if (EditorGUI.EndChangeCheck() && isSingletonInstance) { ClearExportWindowSettings(ConvertToPrefabEditorWindow.k_SessionStoragePrefix); } EditorGUI.indentLevel--; } // -------------------------- EditorGUI.indentLevel--; EditorGUILayout.LabelField("Integration", EditorStyles.boldLabel); EditorGUI.indentLevel++; GUILayout.BeginHorizontal(); EditorGUILayout.LabelField(Style.Application3D, GUILayout.Width(LabelWidth)); // dropdown to select Maya version to use var options = ExportSettings.GetDCCOptions(); exportSettings.SelectedDCCApp = EditorGUILayout.Popup(exportSettings.SelectedDCCApp, options); EditorGUI.BeginDisabledGroup(!isSingletonInstance); if (GUILayout.Button(new GUIContent("...", "Browse to a 3D application in a non-default location"), EditorStyles.miniButton, GUILayout.Width(BrowseButtonWidth))) { var ext = ""; switch (Application.platform) { case RuntimePlatform.WindowsEditor: ext = "exe"; break; case RuntimePlatform.OSXEditor: ext = "app"; break; default: throw new System.NotImplementedException(); } string dccPath = EditorUtility.OpenFilePanel("Select Digital Content Creation Application", ExportSettings.FirstValidVendorLocation, ext); // check that the path is valid and references the maya executable if (!string.IsNullOrEmpty(dccPath)) { ExportSettings.DCCType foundDCC = ExportSettings.DCCType.Maya; var foundDCCPath = TryFindDCC(dccPath, ext, ExportSettings.DCCType.Maya); if (foundDCCPath == null && Application.platform == RuntimePlatform.WindowsEditor) { foundDCCPath = TryFindDCC(dccPath, ext, ExportSettings.DCCType.Max); foundDCC = ExportSettings.DCCType.Max; } if (foundDCCPath == null) { Debug.LogError(string.Format("Could not find supported 3D application at: \"{0}\"", Path.GetDirectoryName(dccPath))); } else { dccPath = foundDCCPath; ExportSettings.AddDCCOption(dccPath, foundDCC); } Repaint(); } } EditorGUI.EndDisabledGroup(); GUILayout.EndHorizontal(); EditorGUILayout.Space(); GUILayout.BeginHorizontal(); EditorGUILayout.LabelField(Style.KeepOpen, GUILayout.Width(LabelWidth)); exportSettings.LaunchAfterInstallation = EditorGUILayout.Toggle( exportSettings.LaunchAfterInstallation ); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); EditorGUILayout.LabelField(Style.HideNativeMenu, GUILayout.Width(LabelWidth)); exportSettings.HideSendToUnityMenuProperty = EditorGUILayout.Toggle( exportSettings.HideSendToUnityMenuProperty ); GUILayout.EndHorizontal(); EditorGUILayout.Space(); // disable button if no 3D application is available EditorGUI.BeginDisabledGroup(!isSingletonInstance || !ExportSettings.CanInstall()); if (GUILayout.Button(Style.InstallIntegrationContent)) { EditorApplication.delayCall += UnityEditor.Formats.Fbx.Exporter.IntegrationsUI.InstallDCCIntegration; } EditorGUI.EndDisabledGroup(); EditorGUILayout.Space(); EditorGUI.indentLevel--; EditorGUILayout.LabelField("FBX Prefab Component Updater", EditorStyles.boldLabel); EditorGUI.indentLevel++; EditorGUILayout.Space(); EditorGUI.BeginDisabledGroup(!isSingletonInstance); if (GUILayout.Button(Style.RepairMissingScripts)) { var componentUpdater = new UnityEditor.Formats.Fbx.Exporter.RepairMissingScripts(); var filesToRepairCount = componentUpdater.AssetsToRepairCount; var dialogTitle = "FBX Prefab Component Updater"; if (filesToRepairCount > 0) { bool result = UnityEditor.EditorUtility.DisplayDialog(dialogTitle, string.Format("Found {0} prefab(s) and/or scene(s) with components requiring update.\n\n" + "If you choose 'Go Ahead', the FbxPrefab components in these assets " + "will be automatically updated to work with the latest FBX exporter.\n" + "You should make a backup before proceeding.", filesToRepairCount), "I Made a Backup. Go Ahead!", "No Thanks"); if (result) { componentUpdater.ReplaceGUIDInTextAssets(); } else { var assetsToRepair = componentUpdater.GetAssetsToRepair(); Debug.LogFormat("Failed to update the FbxPrefab components in the following files:\n{0}", string.Join("\n", assetsToRepair)); } } else { UnityEditor.EditorUtility.DisplayDialog(dialogTitle, "Couldn't find any prefabs or scenes that require updating", "Ok"); } } EditorGUI.EndDisabledGroup(); EditorGUI.indentLevel--; GUILayout.FlexibleSpace(); GUILayout.EndVertical(); GUILayout.EndScrollView(); if (GUI.changed) { // Only save the settings if we are in the Singleton instance. // Otherwise, the user is editing a preset and we don't need to save. if (isSingletonInstance) { exportSettings.Save(); } } } private static string TryFindDCC(string dccPath, string ext, ExportSettings.DCCType dccType) { string dccName = ""; switch (dccType) { case ExportSettings.DCCType.Maya: dccName = "maya"; break; case ExportSettings.DCCType.Max: dccName = "3dsmax"; break; default: throw new System.NotImplementedException(); } if (Path.GetFileNameWithoutExtension(dccPath).ToLower().Equals(dccName)) { return dccPath; } // clicked on the wrong application, try to see if we can still find // a dcc in this directory. var dccDir = new DirectoryInfo(Path.GetDirectoryName(dccPath)); FileSystemInfo[] files = {}; switch (Application.platform) { case RuntimePlatform.OSXEditor: files = dccDir.GetDirectories("*." + ext); break; case RuntimePlatform.WindowsEditor: files = dccDir.GetFiles("*." + ext); break; default: throw new System.NotImplementedException(); } string newDccPath = null; foreach (var file in files) { var filename = Path.GetFileNameWithoutExtension(file.Name).ToLower(); if (filename.Equals(dccName)) { newDccPath = file.FullName.Replace("\\", "/"); break; } } return newDccPath; } [SettingsProvider] static SettingsProvider CreateFbxExportSettingsProvider() { ExportSettings.instance.name = "FBX Export Settings"; ExportSettings.instance.Load(); var provider = AssetSettingsProvider.CreateProviderFromObject( "Project/Fbx Export", ExportSettings.instance, GetSearchKeywordsFromGUIContentProperties(typeof(Style))); #if UNITY_2019_1_OR_NEWER provider.inspectorUpdateHandler += () => { if (provider.settingsEditor != null && provider.settingsEditor.serializedObject.UpdateIfRequiredOrScript()) { provider.Repaint(); } }; #else provider.activateHandler += (searchContext, rootElement) => { if (provider.settingsEditor != null && provider.settingsEditor.serializedObject.UpdateIfRequiredOrScript()) { provider.Repaint(); } }; #endif // UNITY_2019_1_OR_NEWER return provider; } static IEnumerable GetSearchKeywordsFromGUIContentProperties(Type type) { return type.GetFields(BindingFlags.Static | BindingFlags.Public) .Where(field => typeof(GUIContent).IsAssignableFrom(field.FieldType)) .Select(field => ((GUIContent)field.GetValue(null)).text) .Concat(type.GetProperties(BindingFlags.Static | BindingFlags.Public) .Where(prop => typeof(GUIContent).IsAssignableFrom(prop.PropertyType)) .Select(prop => ((GUIContent)prop.GetValue(null, null)).text)) .Where(content => content != null) .Select(content => content.ToLowerInvariant()) .Distinct(); } } [FilePath("ProjectSettings/FbxExportSettings.asset", FilePathAttribute.Location.ProjectFolder)] internal class ExportSettings : ScriptableObject { internal const string kDefaultSavePath = "."; private static List s_PreferenceList = new List() {kMayaOptionName, kMayaLtOptionName, kMaxOptionName}; //Any additional names require a space after the name internal const string kMaxOptionName = "3ds Max "; internal const string kMayaOptionName = "Maya "; internal const string kMayaLtOptionName = "Maya LT"; private static ExportSettings s_Instance; public static ExportSettings instance { get { if (s_Instance == null) { s_Instance = ScriptableObject.CreateInstance(); s_Instance.Load(); // don't save the export settings to the scene, // otherwise they could be deleted on new scene, breaking // the UI if the settings are being inspected. s_Instance.hideFlags = HideFlags.DontSaveInEditor; } return s_Instance; } } internal ExportSettings() { if (s_Instance != null) { // The user has most likely created a preset. } } internal void SaveToFile() { if (s_Instance == null) { Debug.Log("Cannot save ScriptableSingleton: no instance!"); return; } string filePath = GetFilePath(); if (!string.IsNullOrEmpty(filePath)) { string directoryName = Path.GetDirectoryName(filePath); if (!Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } System.IO.File.WriteAllText(filePath, EditorJsonUtility.ToJson(s_Instance, true)); } } private static string GetFilePath() { foreach (var attr in typeof(ExportSettings).GetCustomAttributes(true)) { FilePathAttribute filePathAttribute = attr as FilePathAttribute; if (filePathAttribute != null) { return filePathAttribute.filepath; } } return null; } // NOTE: using "Verbose" and "VerboseProperty" to handle backwards compatibility with older FbxExportSettings.asset files. // The variable name is used when serializing, so changing the variable name would prevent older FbxExportSettings.asset files // from loading this property. [SerializeField] private bool Verbose = false; internal bool VerboseProperty { get { return Verbose; } set { Verbose = value; } } private static string DefaultIntegrationSavePath { get { return Path.GetDirectoryName(Application.dataPath); } } private static string GetMayaLocationFromEnvironmentVariable(string env) { string result = null; if (string.IsNullOrEmpty(env)) return null; string location = Environment.GetEnvironmentVariable(env); if (string.IsNullOrEmpty(location)) return null; //Remove any extra slashes on the end //Maya would accept a single slash in either direction, so we should be able to location = location.Replace("\\", "/"); location = location.TrimEnd('/'); if (!Directory.Exists(location)) return null; if (Application.platform == RuntimePlatform.WindowsEditor) { //If we are on Windows, we need only go up one location to get to the "Autodesk" folder. result = Directory.GetParent(location).ToString(); } else if (Application.platform == RuntimePlatform.OSXEditor) { //We can assume our path is: /Applications/Autodesk/maya2017/Maya.app/Contents //So we need to go up three folders. var appFolder = Directory.GetParent(location); if (appFolder != null) { var versionFolder = Directory.GetParent(appFolder.ToString()); if (versionFolder != null) { var autoDeskFolder = Directory.GetParent(versionFolder.ToString()); if (autoDeskFolder != null) { result = autoDeskFolder.ToString(); } } } } return NormalizePath(result, false); } /// /// Returns a set of valid vendor folder paths with no trailing '/' /// private static HashSet GetCustomVendorLocations() { HashSet result = null; var environmentVariable = Environment.GetEnvironmentVariable("UNITY_3DAPP_VENDOR_LOCATIONS"); if (!string.IsNullOrEmpty(environmentVariable)) { result = new HashSet(); string[] locations = environmentVariable.Split(';'); foreach (var location in locations) { if (Directory.Exists(location)) { result.Add(NormalizePath(location, false)); } } } return result; } private static HashSet GetDefaultVendorLocations() { string platformDefault; switch (Application.platform) { case RuntimePlatform.WindowsEditor: platformDefault = "C:/Program Files/Autodesk"; break; case RuntimePlatform.OSXEditor: platformDefault = "/Applications/Autodesk"; break; case RuntimePlatform.LinuxEditor: platformDefault = "/usr/autodesk"; break; default: throw new NotImplementedException(); } HashSet existingDirectories = new HashSet(); if (!string.IsNullOrEmpty(platformDefault) && Directory.Exists(platformDefault)) { existingDirectories.Add(platformDefault); } return existingDirectories; } /// /// Retrieve available vendor locations. /// If there is valid alternative vendor locations, do not use defaults /// always use MAYA_LOCATION when available /// internal static List DCCVendorLocations { get { HashSet result = GetCustomVendorLocations(); if (result == null) { result = GetDefaultVendorLocations(); } var additionalLocation = GetMayaLocationFromEnvironmentVariable("MAYA_LOCATION"); if (!string.IsNullOrEmpty(additionalLocation)) { result.Add(additionalLocation); } return result.ToList(); } } [SerializeField] private bool launchAfterInstallation = true; public bool LaunchAfterInstallation { get { return launchAfterInstallation; } set { launchAfterInstallation = value; } } [SerializeField] private bool HideSendToUnityMenu = true; public bool HideSendToUnityMenuProperty { get { return HideSendToUnityMenu; } set { HideSendToUnityMenu = value; } } [SerializeField] private bool BakeAnimation = false; internal bool BakeAnimationProperty { get { return BakeAnimation; } set { BakeAnimation = value; } } [SerializeField] private bool showConvertToPrefabDialog = true; public bool DisplayOptionsWindow { get { return showConvertToPrefabDialog; } set { showConvertToPrefabDialog = value; } } [SerializeField] private string integrationSavePath; internal static string IntegrationSavePath { get { //If the save path gets messed up and ends up not being valid, just use the project folder as the default if (string.IsNullOrEmpty(instance.integrationSavePath) || !Directory.Exists(instance.integrationSavePath)) { //The project folder, above the asset folder instance.integrationSavePath = DefaultIntegrationSavePath; } return instance.integrationSavePath; } set { instance.integrationSavePath = value; } } [SerializeField] private int selectedDCCApp = 0; internal int SelectedDCCApp { get { return selectedDCCApp; } set { selectedDCCApp = value; } } /// /// The path where Convert To Model will save the new fbx and prefab. /// /// To help teams work together, this is stored to be relative to the /// Application.dataPath, and the path separator is the forward-slash /// (e.g. unix and http, not windows). /// /// Use GetRelativeSavePath / SetRelativeSavePath to get/set this /// value, properly interpreted for the current platform. /// [SerializeField] private List prefabSavePaths = new List(); internal List GetCopyOfPrefabSavePaths() { return new List(prefabSavePaths); } [SerializeField] private List fbxSavePaths = new List(); internal List GetCopyOfFbxSavePaths() { return new List(fbxSavePaths); } [SerializeField] private int selectedFbxPath = 0; public int SelectedFbxPath { get { return selectedFbxPath; } set { selectedFbxPath = value; } } [SerializeField] private int selectedPrefabPath = 0; public int SelectedPrefabPath { get { return selectedPrefabPath; } set { selectedPrefabPath = value; } } private int maxStoredSavePaths = 5; // List of names in order that they appear in option list [SerializeField] private List dccOptionNames = new List(); // List of paths in order that they appear in the option list [SerializeField] private List dccOptionPaths; // don't serialize as ScriptableObject does not get properly serialized on export [System.NonSerialized] private ExportModelSettings m_exportModelSettings; internal ExportModelSettings ExportModelSettings { get { if (!m_exportModelSettings) { m_exportModelSettings = ScriptableObject.CreateInstance(typeof(ExportModelSettings)) as ExportModelSettings; } if (this.exportModelSettingsSerialize != null) { m_exportModelSettings.info = this.exportModelSettingsSerialize; } else { this.exportModelSettingsSerialize = m_exportModelSettings.info; } return m_exportModelSettings; } set { m_exportModelSettings = value; } } // store contents of export model settings for serialization [SerializeField] private ExportModelSettingsSerialize exportModelSettingsSerialize; [System.NonSerialized] private ConvertToPrefabSettings m_convertToPrefabSettings; internal ConvertToPrefabSettings ConvertToPrefabSettings { get { if (!m_convertToPrefabSettings) { m_convertToPrefabSettings = ScriptableObject.CreateInstance(typeof(ConvertToPrefabSettings)) as ConvertToPrefabSettings; } if (this.convertToPrefabSettingsSerialize != null) { m_convertToPrefabSettings.info = this.convertToPrefabSettingsSerialize; } else { this.convertToPrefabSettingsSerialize = m_convertToPrefabSettings.info; } return m_convertToPrefabSettings; } set { m_convertToPrefabSettings = value; } } [SerializeField] private ConvertToPrefabSettingsSerialize convertToPrefabSettingsSerialize; internal void LoadDefaults() { LaunchAfterInstallation = true; HideSendToUnityMenuProperty = true; prefabSavePaths = new List(){ kDefaultSavePath }; fbxSavePaths = new List(){ kDefaultSavePath }; integrationSavePath = DefaultIntegrationSavePath; dccOptionPaths = new List(); dccOptionNames = new List(); BakeAnimationProperty = false; exportModelSettingsSerialize = new ExportModelSettingsSerialize(); ExportModelSettings.info = exportModelSettingsSerialize; DisplayOptionsWindow = true; convertToPrefabSettingsSerialize = new ConvertToPrefabSettingsSerialize(); ConvertToPrefabSettings.info = convertToPrefabSettingsSerialize; } /// /// Increments the name if there is a duplicate in dccAppOptions. /// /// The unique name. /// Name. internal static string GetUniqueDCCOptionName(string name) { Debug.Assert(instance != null); if (name == null) { return null; } if (!instance.dccOptionNames.Contains(name)) { return name; } var format = "{1} ({0})"; int index = 1; // try extracting the current index from the name and incrementing it var result = System.Text.RegularExpressions.Regex.Match(name, @"\((?\d+?)\)$"); if (result != null) { var number = result.Groups["number"].Value; int tempIndex; if (int.TryParse(number, out tempIndex)) { var indexOfNumber = name.LastIndexOf(number); format = name.Remove(indexOfNumber, number.Length).Insert(indexOfNumber, "{0}"); index = tempIndex + 1; } } string uniqueName = null; do { uniqueName = string.Format(format, index, name); index++; } while (instance.dccOptionNames.Contains(uniqueName)); return uniqueName; } internal void SetDCCOptionNames(List newList) { dccOptionNames = newList; } internal void SetDCCOptionPaths(List newList) { dccOptionPaths = newList; } internal void ClearDCCOptionNames() { dccOptionNames.Clear(); } internal void ClearDCCOptions() { SetDCCOptionNames(null); SetDCCOptionPaths(null); } /// /// /// Find the latest program available and make that the default choice. /// Will always take any Maya version over any 3ds Max version. /// /// Returns the index of the most recent program in the list of dccOptionNames /// Returns -1 on error. /// internal int PreferredDCCApp { get { if (dccOptionNames == null) { return -1; } int result = -1; int newestDCCVersionNumber = -1; for (int i = 0; i < dccOptionNames.Count; i++) { int versionToCheck = FindDCCVersion(dccOptionNames[i]); if (versionToCheck == -1) { if (dccOptionNames[i] == "MAYA_LOCATION") return i; continue; } if (versionToCheck > newestDCCVersionNumber) { result = i; newestDCCVersionNumber = versionToCheck; } else if (versionToCheck == newestDCCVersionNumber) { int selection = ChoosePreferredDCCApp(result, i); if (selection == i) { result = i; newestDCCVersionNumber = FindDCCVersion(dccOptionNames[i]); } } } return result; } } /// /// Takes the index of two program names from dccOptionNames and chooses our preferred one based on the preference list /// This happens in case of a tie between two programs with the same release year / version /// /// /// /// private int ChoosePreferredDCCApp(int optionA, int optionB) { Debug.Assert(optionA >= 0 && optionB >= 0 && optionA < dccOptionNames.Count && optionB < dccOptionNames.Count); if (dccOptionNames.Count == 0) { return -1; } var appA = dccOptionNames[optionA]; var appB = dccOptionNames[optionB]; if (appA == null || appB == null || appA.Length <= 0 || appB.Length <= 0) { return -1; } int scoreA = s_PreferenceList.FindIndex(app => RemoveSpacesAndNumbers(app).Equals(RemoveSpacesAndNumbers(appA))); int scoreB = s_PreferenceList.FindIndex(app => RemoveSpacesAndNumbers(app).Equals(RemoveSpacesAndNumbers(appB))); return scoreA < scoreB ? optionA : optionB; } /// /// Takes a given string and removes any spaces or numbers from it /// /// internal static string RemoveSpacesAndNumbers(string s) { return System.Text.RegularExpressions.Regex.Replace(s, @"[\s^0-9]", ""); } /// /// Finds the version based off of the title of the application /// /// /// the year/version OR -1 if the year could not be parsed private static int FindDCCVersion(string AppName) { if (string.IsNullOrEmpty(AppName)) { return -1; } AppName = AppName.Trim(); if (string.IsNullOrEmpty(AppName)) return -1; string[] piecesArray = AppName.Split(' '); if (piecesArray.Length < 2) { return -1; } //Get the number, which is always the last chunk separated by a space. string number = piecesArray[piecesArray.Length - 1]; int version; if (int.TryParse(number, out version)) { return version; } else { //remove any letters in the string in a final attempt to extract an int from it (this will happen with MayaLT, for example) string AppNameCopy = AppName; string stringWithoutLetters = System.Text.RegularExpressions.Regex.Replace(AppNameCopy, "[^0-9]", ""); if (int.TryParse(stringWithoutLetters, out version)) { return version; } float fVersion; //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. if (float.TryParse(number, out fVersion)) { return (int)fVersion; } return -1; } } /// /// Find Maya and 3DsMax installations at default install path. /// Add results to given dictionary. /// /// If MAYA_LOCATION is set, add this to the list as well. /// private static void FindDCCInstalls() { var dccOptionNames = instance.dccOptionNames; var dccOptionPaths = instance.dccOptionPaths; // find dcc installation from vendor locations foreach (var vendorLocation in DCCVendorLocations) { if (!Directory.Exists(vendorLocation)) { // no autodesk products installed continue; } // List that directory and find the right version: // either the newest version, or the exact version we wanted. var adskRoot = new System.IO.DirectoryInfo(vendorLocation); foreach (var productDir in adskRoot.GetDirectories()) { var product = productDir.Name; // Only accept those that start with 'maya' in either case. if (product.StartsWith("maya", StringComparison.InvariantCultureIgnoreCase)) { string version = product.Substring("maya".Length); dccOptionPaths.Add(GetMayaExePathFromLocation(productDir.FullName.Replace("\\", "/"))); dccOptionNames.Add(GetUniqueDCCOptionName(kMayaOptionName + version)); continue; } if (product.StartsWith("3ds max", StringComparison.InvariantCultureIgnoreCase)) { var exePath = string.Format("{0}/{1}", productDir.FullName.Replace("\\", "/"), "3dsmax.exe"); string version = product.Substring("3ds max ".Length); var maxOptionName = GetUniqueDCCOptionName(kMaxOptionName + version); if (IsEarlierThanMax2017(maxOptionName)) { continue; } dccOptionPaths.Add(exePath); dccOptionNames.Add(maxOptionName); } } } // add extra locations defined by special environment variables string location = GetMayaLocationFromEnvironmentVariable("MAYA_LOCATION"); if (!string.IsNullOrEmpty(location)) { dccOptionPaths.Add(GetMayaExePathFromLocation(location)); dccOptionNames.Add("MAYA_LOCATION"); } instance.SelectedDCCApp = instance.PreferredDCCApp; } /// /// Returns the first valid folder in our list of vendor locations /// /// The first valid vendor location internal static string FirstValidVendorLocation { get { List locations = DCCVendorLocations; for (int i = 0; i < locations.Count; i++) { //Look through the list of locations we have and take the first valid one if (Directory.Exists(locations[i])) { return locations[i]; } } //if no valid locations exist, just take us to the project folder return Directory.GetCurrentDirectory(); } } /// /// Gets the maya exe at Maya install location. /// /// The maya exe path. /// Location of Maya install. private static string GetMayaExePathFromLocation(string location) { switch (Application.platform) { case RuntimePlatform.WindowsEditor: return $"{location}/bin/maya.exe"; case RuntimePlatform.OSXEditor: // MAYA_LOCATION on mac is set by Autodesk to be the // Contents directory. But let's make it easier on people // and allow just having it be the app bundle or a // directory that holds the app bundle. if (location.EndsWith(".app/Contents")) { return $"{location}/MacOS/Maya"; } else if (location.EndsWith(".app")) { return $"{location}/Contents/MacOS/Maya"; } else { return $"{location}/Maya.app/Contents/MacOS/Maya"; } case RuntimePlatform.LinuxEditor: return $"{location}/bin/maya"; default: throw new NotImplementedException(); } } internal static GUIContent[] GetDCCOptions() { if (instance.dccOptionNames == null || instance.dccOptionNames.Count != instance.dccOptionPaths.Count || instance.dccOptionNames.Count == 0) { instance.dccOptionPaths = new List(); instance.dccOptionNames = new List(); FindDCCInstalls(); } // store the selected app if any string prevSelection = SelectedDCCPath; // remove options that no longer exist List pathsToDelete = new List(); List namesToDelete = new List(); for (int i = 0; i < instance.dccOptionPaths.Count; i++) { var dccPath = instance.dccOptionPaths[i]; if (!File.Exists(dccPath)) { namesToDelete.Add(instance.dccOptionNames[i]); pathsToDelete.Add(dccPath); } } foreach (var str in pathsToDelete) { instance.dccOptionPaths.Remove(str); } foreach (var str in namesToDelete) { instance.dccOptionNames.Remove(str); } // set the selected DCC app to the previous selection instance.SelectedDCCApp = instance.dccOptionPaths.IndexOf(prevSelection); if (instance.SelectedDCCApp < 0) { // find preferred app if previous selection no longer exists instance.SelectedDCCApp = instance.PreferredDCCApp; } if (instance.dccOptionPaths.Count <= 0) { instance.SelectedDCCApp = 0; return new GUIContent[] { new GUIContent("") }; } GUIContent[] optionArray = new GUIContent[instance.dccOptionPaths.Count]; for (int i = 0; i < instance.dccOptionPaths.Count; i++) { optionArray[i] = new GUIContent( instance.dccOptionNames[i], instance.dccOptionPaths[i] ); } return optionArray; } internal enum DCCType { Maya, Max }; internal static void AddDCCOption(string newOption, DCCType dcc) { if (Application.platform == RuntimePlatform.OSXEditor && dcc == DCCType.Maya) { // on OSX we get a path ending in .app, which is not quite the exe newOption = GetMayaExePathFromLocation(newOption); } var dccOptionPaths = instance.dccOptionPaths; if (dccOptionPaths.Contains(newOption)) { instance.SelectedDCCApp = dccOptionPaths.IndexOf(newOption); return; } string optionName = ""; switch (dcc) { case DCCType.Maya: var version = AskMayaVersion(newOption); if (version == null) { Debug.LogError("This version of Maya could not be launched properly"); UnityEditor.EditorUtility.DisplayDialog("Error Loading 3D Application", "Failed to add Maya option, could not get version number from maya.exe", "Ok"); return; } optionName = GetUniqueDCCOptionName("Maya " + version); break; case DCCType.Max: optionName = GetMaxOptionName(newOption); if (ExportSettings.IsEarlierThanMax2017(optionName)) { Debug.LogError("Earlier than 3ds Max 2017 is not supported"); UnityEditor.EditorUtility.DisplayDialog( "Error adding 3D Application", "Unity Integration only supports 3ds Max 2017 or later", "Ok"); return; } break; default: throw new System.NotImplementedException(); } instance.dccOptionNames.Add(optionName); dccOptionPaths.Add(newOption); instance.SelectedDCCApp = dccOptionPaths.Count - 1; } /// /// Ask the version number by running maya. /// internal static string AskMayaVersion(string exePath) { System.Diagnostics.Process myProcess = new System.Diagnostics.Process(); myProcess.StartInfo.FileName = exePath; myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; myProcess.StartInfo.CreateNoWindow = true; myProcess.StartInfo.UseShellExecute = false; myProcess.StartInfo.RedirectStandardOutput = true; myProcess.StartInfo.Arguments = "-v"; myProcess.EnableRaisingEvents = true; myProcess.Start(); string resultString = myProcess.StandardOutput.ReadToEnd(); myProcess.WaitForExit(); // Output is like: Maya 2018, Cut Number 201706261615 // We want the stuff after 'Maya ' and before the comma. // (Uni-31601) less brittle! Consider also the mel command "about -version". if (string.IsNullOrEmpty(resultString)) { return null; } resultString = resultString.Trim(); var commaIndex = resultString.IndexOf(','); if (commaIndex != -1) { const int versionStart = 5; // length of "Maya " return resultString.Length > versionStart ? resultString.Substring(0, commaIndex).Substring(versionStart) : null; } else { //This probably means we tried to launch Maya to check the version but it was some sort of broken maya. //We'll just return null and throw an error for it. return null; } } /// /// Gets the unique label for a new 3DsMax dropdown option. /// /// The 3DsMax dropdown option label. /// Exe path. internal static string GetMaxOptionName(string exePath) { return GetUniqueDCCOptionName(Path.GetFileName(Path.GetDirectoryName(exePath))); } internal static bool IsEarlierThanMax2017(string AppName) { int version = FindDCCVersion(AppName); return version != -1 && version < 2017; } internal static string SelectedDCCPath { get { return (instance.dccOptionPaths.Count > 0 && instance.SelectedDCCApp >= 0 && instance.SelectedDCCApp < instance.dccOptionPaths.Count) ? instance.dccOptionPaths[instance.SelectedDCCApp] : ""; } } internal static string SelectedDCCName { get { return (instance.dccOptionNames.Count > 0 && instance.SelectedDCCApp >= 0 && instance.SelectedDCCApp < instance.dccOptionNames.Count) ? instance.dccOptionNames[instance.SelectedDCCApp] : ""; } } internal static bool CanInstall() { return instance.dccOptionPaths.Count > 0; } internal static string GetProjectRelativePath(string fullPath) { var assetRelativePath = UnityEditor.Formats.Fbx.Exporter.ExportSettings.ConvertToAssetRelativePath(fullPath); var projectRelativePath = "Assets/" + assetRelativePath; if (string.IsNullOrEmpty(assetRelativePath)) { throw new FbxExportSettingsException("Path " + fullPath + " must be in the Assets folder."); } return projectRelativePath; } /// /// The relative save paths for given absolute paths. /// This is relative to the Application.dataPath ; it uses '/' as the /// separator on all platforms. /// internal static string[] GetRelativeSavePaths(List exportSavePaths) { if (exportSavePaths == null) { return null; } if (exportSavePaths.Count == 0) { exportSavePaths.Add(kDefaultSavePath); } string[] relSavePaths = new string[exportSavePaths.Count]; // use special forward slash unicode char as "/" is a special character // that affects the dropdown layout. string forwardslash = " \u2044 "; for (int i = 0; i < relSavePaths.Length; i++) { relSavePaths[i] = string.Format("Assets{0}{1}", forwardslash, exportSavePaths[i] == "." ? "" : NormalizePath(exportSavePaths[i], isRelative: true).Replace("/", forwardslash)); } return relSavePaths; } /// /// Returns the paths for display in the menu. /// Paths inside the Assets folder are relative, while those outside are kept absolute. /// internal static string[] GetMixedSavePaths(List exportSavePaths) { string[] displayPaths = new string[exportSavePaths.Count]; string forwardslash = " \u2044 "; for (int i = 0; i < displayPaths.Length; i++) { // if path is in Assets folder, shorten it if (!Path.IsPathRooted(exportSavePaths[i])) { displayPaths[i] = string.Format("Assets{0}{1}", forwardslash, exportSavePaths[i] == "." ? "" : NormalizePath(exportSavePaths[i], isRelative: true).Replace("/", forwardslash)); } else { displayPaths[i] = exportSavePaths[i].Replace("/", forwardslash); } } return displayPaths; } /// /// The path where Export model will save the new fbx. /// This is relative to the Application.dataPath ; it uses '/' as the /// separator on all platforms. /// Only returns the paths within the Assets folder of the project. /// internal static string[] GetRelativeFbxSavePaths() { return GetRelativeFbxSavePaths(instance.fbxSavePaths, ref instance.selectedFbxPath); } /// /// The path where Export model will save the new fbx. /// This is relative to the Application.dataPath ; it uses '/' as the /// separator on all platforms. /// Only returns the paths within the Assets folder of the project. /// internal static string[] GetRelativeFbxSavePaths(List fbxSavePaths, ref int pathIndex) { // sort the list of paths, putting project paths first fbxSavePaths.Sort((x, y) => Path.IsPathRooted(x).CompareTo(Path.IsPathRooted(y))); var relPathCount = fbxSavePaths.FindAll(x => !Path.IsPathRooted(x)).Count; // reset selected path if it's out of range if (pathIndex > relPathCount - 1) { pathIndex = 0; } return GetRelativeSavePaths(fbxSavePaths.GetRange(0, relPathCount)); } /// /// The path where Convert to Prefab will save the new prefab. /// This is relative to the Application.dataPath ; it uses '/' as the /// separator on all platforms. /// internal static string[] GetRelativePrefabSavePaths() { return GetRelativeSavePaths(instance.prefabSavePaths); } /// /// The paths formatted for display in the menu. /// Paths outside the Assets folder are kept as they are and ones inside are shortened. /// internal static string[] GetMixedFbxSavePaths() { return GetMixedSavePaths(instance.fbxSavePaths); } /// /// Adds the save path to given save path list. /// /// Save path. /// Export save paths. internal static void AddSavePath(string savePath, List exportSavePaths, bool exportOutsideProject = false) { if (exportSavePaths == null) { return; } if (exportOutsideProject) { savePath = NormalizePath(savePath, isRelative: false); } else { savePath = NormalizePath(savePath, isRelative: true); } if (exportSavePaths.Contains(savePath)) { // move to first place if it isn't already if (exportSavePaths[0] == savePath) { return; } exportSavePaths.Remove(savePath); } if (exportSavePaths.Count >= instance.maxStoredSavePaths) { // remove last used path exportSavePaths.RemoveAt(exportSavePaths.Count - 1); } exportSavePaths.Insert(0, savePath); } internal static void AddFbxSavePath(string savePath, bool exportOutsideProject = false) { AddSavePath(savePath, instance.fbxSavePaths, exportOutsideProject); instance.SelectedFbxPath = 0; } internal static void AddPrefabSavePath(string savePath) { AddSavePath(savePath, instance.prefabSavePaths); instance.SelectedPrefabPath = 0; } internal static string GetAbsoluteSavePath(string savePath) { var projectAbsolutePath = Path.Combine(Application.dataPath, savePath); projectAbsolutePath = NormalizePath(projectAbsolutePath, isRelative: false, separator: Path.DirectorySeparatorChar); // if path is outside Assets folder, it's already absolute so return the original path if (string.IsNullOrEmpty(ExportSettings.ConvertToAssetRelativePath(projectAbsolutePath))) { return savePath; } return projectAbsolutePath; } internal static string FbxAbsoluteSavePath { get { if (instance.fbxSavePaths.Count <= 0) { instance.fbxSavePaths.Add(kDefaultSavePath); } return GetAbsoluteSavePath(instance.fbxSavePaths[instance.SelectedFbxPath]); } } internal static string PrefabAbsoluteSavePath { get { if (instance.prefabSavePaths.Count <= 0) { instance.prefabSavePaths.Add(kDefaultSavePath); } return GetAbsoluteSavePath(instance.prefabSavePaths[instance.SelectedPrefabPath]); } } /// /// Convert an absolute path into a relative path like what you would /// get from GetRelativeSavePath. /// /// This uses '/' as the path separator. /// /// If 'requireSubdirectory' is the default on, return empty-string if the full /// path is not in a subdirectory of assets. /// internal static string ConvertToAssetRelativePath(string fullPathInAssets, bool requireSubdirectory = true) { if (!Path.IsPathRooted(fullPathInAssets)) { fullPathInAssets = Path.GetFullPath(fullPathInAssets); } var relativePath = GetRelativePath(Application.dataPath, fullPathInAssets); if (requireSubdirectory && relativePath.StartsWith("..")) { if (relativePath.Length == 2 || relativePath[2] == '/') { // The relative path has us pop out to another directory, // so return an empty string as requested. return ""; } } return relativePath; } /// /// Compute how to get from 'fromDir' to 'toDir' via a relative path. /// internal static string GetRelativePath(string fromDir, string toDir, char separator = '/') { // https://stackoverflow.com/questions/275689/how-to-get-relative-path-from-absolute-path // Except... the MakeRelativeUri that ships with Unity is buggy. // e.g. https://bugzilla.xamarin.com/show_bug.cgi?id=5921 // among other bugs. So we roll our own. // Normalize the paths, assuming they're absolute paths (if they // aren't, they get normalized as relative paths) fromDir = NormalizePath(fromDir, isRelative: false); toDir = NormalizePath(toDir, isRelative: false); // Break them into path components. var fromDirs = fromDir.Split('/'); var toDirs = toDir.Split('/'); // Find the least common ancestor int lca = -1; for (int i = 0, n = System.Math.Min(fromDirs.Length, toDirs.Length); i < n; ++i) { if (fromDirs[i] != toDirs[i]) { break; } lca = i; } // Step up from the fromDir to the lca, then down from lca to the toDir. // If from = /a/b/c/d // and to = /a/b/e/f/g // Then we need to go up 2 and down 3. var nStepsUp = (fromDirs.Length - 1) - lca; var nStepsDown = (toDirs.Length - 1) - lca; if (nStepsUp + nStepsDown == 0) { return "."; } var relDirs = new string[nStepsUp + nStepsDown]; for (int i = 0; i < nStepsUp; ++i) { relDirs[i] = ".."; } for (int i = 0; i < nStepsDown; ++i) { relDirs[nStepsUp + i] = toDirs[lca + 1 + i]; } return string.Join("" + separator, relDirs); } /// /// Normalize a path, cleaning up path separators, resolving '.' and /// '..', removing duplicate and trailing path separators, etc. /// /// If the path passed in is a relative path, we remove leading path separators. /// If it's an absolute path we don't. /// /// If you claim the path is absolute but actually it's relative, we /// treat it as a relative path. /// internal static string NormalizePath(string path, bool isRelative, char separator = '/') { if (path == null) { return null; } // Use slashes to simplify the code (we're going to clobber them all anyway). path = path.Replace('\\', '/'); // If we're supposed to be an absolute path, but we're actually a // relative path, ignore the 'isRelative' flag. if (!isRelative && !Path.IsPathRooted(path)) { isRelative = true; } // Build up a list of directory items. var dirs = path.Split('/'); // Modify dirs in-place, reading from readIndex and remembering // what index we've written to. int lastWriteIndex = -1; for (int readIndex = 0, n = dirs.Length; readIndex < n; ++readIndex) { var dir = dirs[readIndex]; // Skip duplicate path separators. if (string.IsNullOrEmpty(dir)) { // Skip if it's not a leading path separator. if (lastWriteIndex >= 0) { continue; } // Also skip if it's leading and we have a relative path. if (isRelative) { continue; } } // Skip '.' if (dir == ".") { continue; } // Erase the previous directory we read on '..'. // Exception: we can start with '..' // Exception: we can have multiple '..' in a row. // // Note: this ignores the actual file system and the funny // results you see when there are symlinks. if (dir == "..") { if (lastWriteIndex == -1) { // Leading '..' => handle like a normal directory. } else if (dirs[lastWriteIndex] == "..") { // Multiple ".." => handle like a normal directory. } else { // Usual case: delete the previous directory. lastWriteIndex--; continue; } } // Copy anything else to the next index. ++lastWriteIndex; dirs[lastWriteIndex] = dirs[readIndex]; } if (lastWriteIndex == -1 || (lastWriteIndex == 0 && string.IsNullOrEmpty(dirs[lastWriteIndex]))) { // If we didn't keep anything, we have the empty path. // For an absolute path that's / ; for a relative path it's . if (isRelative) { return "."; } else { return "" + separator; } } else { // Otherwise print out the path with the proper separator. return String.Join("" + separator, dirs, 0, lastWriteIndex + 1); } } internal void Load() { string filePath = GetFilePath(); if (!System.IO.File.Exists(filePath)) { LoadDefaults(); } else { try { var fileData = System.IO.File.ReadAllText(filePath); EditorJsonUtility.FromJsonOverwrite(fileData, s_Instance); } catch (Exception xcp) { // Quash the exception and take the default settings. Debug.LogException(xcp); LoadDefaults(); } } } internal void Save() { exportModelSettingsSerialize = ExportModelSettings.info; convertToPrefabSettingsSerialize = ConvertToPrefabSettings.info; this.SaveToFile(); } // Called on creation and whenever the reset button is clicked internal void Reset() { // apply default settings LoadDefaults(); } } [AttributeUsage(AttributeTargets.Class)] internal sealed class FilePathAttribute : Attribute { public enum Location { PreferencesFolder, ProjectFolder } public string filepath { get; set; } public FilePathAttribute(string relativePath, FilePathAttribute.Location location) { if (string.IsNullOrEmpty(relativePath)) { Debug.LogError("Invalid relative path! (its null or empty)"); return; } if (relativePath[0] == '/') { relativePath = relativePath.Substring(1); } if (location == FilePathAttribute.Location.PreferencesFolder) { this.filepath = InternalEditorUtility.unityPreferencesFolder + "/" + relativePath; } else { this.filepath = relativePath; } } } }