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;
}
}
}
}