Initial Commit

This commit is contained in:
jellejurre
2025-07-19 01:03:02 +02:00
commit e7904e3140
304 changed files with 22521 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7698f40b260788141b158264be2f08e9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,662 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEditor;
using UnityEditor.Presets;
using UnityEditor.Animations;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using Object = UnityEngine.Object;
namespace DreadScripts.AssetOrganizer
{
public class AssetOrganizer : EditorWindow
{
#region Declarations
#region Constants
private const string PrefsKey = "AvatarAssetOrganizerSettings";
private static readonly OrganizeType[] organizeTypes =
{
new OrganizeType(0, "Animation", typeof(AnimationClip), typeof(BlendTree)),
new OrganizeType(1, "Controller", typeof(AnimatorController), typeof(AnimatorOverrideController)),
new OrganizeType(2, "Texture", typeof(Texture)),
new OrganizeType(3, "Material", typeof(Material)),
new OrganizeType(4, "Model", new string[] {".fbx",".obj", ".dae", ".3ds", ".dxf"}, typeof(Mesh)),
new OrganizeType(5, "Prefab", new string[] {".prefab"}, typeof(GameObject)),
new OrganizeType(6, "Audio", typeof(AudioClip)),
new OrganizeType(7, "Mask", typeof(AvatarMask)),
new OrganizeType(8, "Scene", typeof(SceneAsset)),
new OrganizeType(9, "Preset", typeof(Preset)),
new OrganizeType(10, "VRC", "VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionParameters","VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu"),
new OrganizeType(11, "Shader", typeof(Shader)),
new OrganizeType(12, "Script", new string[] {".dll"}, typeof(MonoScript)),
new OrganizeType(13, "Other", typeof(ScriptableObject))
};
private static readonly string[] mainTabs = {"Organizer", "Options"};
private static readonly string[] optionTabs = {"Folders", "Types"};
private enum OrganizeAction
{
Skip,
Move
//Removed till an intuitive way for user friendly GUID replacement option is implemented
//Copy
}
private enum SortOptions
{
AlphabeticalPath,
AlphabeticalAsset,
AssetType
}
#endregion
#region Automated Variables
private static int mainToolbarIndex;
private static int optionsToolbarIndex;
private static DependencyAsset[] assets;
private static List<string> createdFolders = new List<string>();
private Vector2 scrollview;
#endregion
#region Input
private static Object mainAsset;
private static string destinationPath;
[SerializeField] private List<CustomFolder> specialFolders;
[SerializeField] private OrganizeAction[] typeActions;
[SerializeField] private SortOptions sortByOption;
[SerializeField] private bool deleteEmptyFolders = true;
#endregion
#endregion
#region Methods
#region Main Methods
private void OnGUI()
{
scrollview = EditorGUILayout.BeginScrollView(scrollview);
mainToolbarIndex = GUILayout.Toolbar(mainToolbarIndex, mainTabs);
switch (mainToolbarIndex)
{
case 0:
DrawOrganizerTab();
break;
case 1:
DrawOptionsTab();
break;
}
DrawSeparator();
Credit();
EditorGUILayout.EndScrollView();
}
private void GetDependencyAssets()
{
destinationPath = AssetDatabase.GetAssetPath(mainAsset);
bool isFolder = AssetDatabase.IsValidFolder(destinationPath);
string[] assetsPath = isFolder ? GetAssetPathsInFolder(destinationPath).ToArray() : AssetDatabase.GetDependencies(destinationPath);
assets = assetsPath.Select(p => new DependencyAsset(p)).ToArray();
if (!isFolder) destinationPath = destinationPath.Replace('\\', '/').Substring(0, destinationPath.LastIndexOf('/'));
foreach (var a in assets)
{
string[] subFolders = a.path.Split('/');
bool setByFolder = false;
foreach (var f in specialFolders)
{
if (!f.active) continue;
if (subFolders.All(s => s != f.name)) continue;
a.action = f.action;
setByFolder = true;
break;
}
if (setByFolder) continue;
if (!TrySetAction(a))
a.associatedType = organizeTypes.Last();
}
switch (sortByOption)
{
case SortOptions.AlphabeticalPath:
assets = assets.OrderBy(a => a.path).ToArray();
break;
case SortOptions.AlphabeticalAsset:
assets = assets.OrderBy(a => a.asset.name).ToArray();
break;
case SortOptions.AssetType:
assets = assets.OrderBy(a => a.type.Name).ToArray();
break;
}
}
private void OrganizeAssets()
{
CheckFolders();
List<string> affectedFolders = new List<string>();
try
{
AssetDatabase.StartAssetEditing();
int count = assets.Length;
float progressPerAsset = 1f / count;
for (var i = 0; i < count; i++)
{
EditorUtility.DisplayProgressBar("Organizing", $"Organizing Assets ({i+1}/{count})", (i + 1) * progressPerAsset);
var a = assets[i];
string newPath = AssetDatabase.GenerateUniqueAssetPath($"{destinationPath}/{a.associatedType.name}/{Path.GetFileName(a.path)}");
switch (a.action)
{
default: case OrganizeAction.Skip: continue;
case OrganizeAction.Move:
AssetDatabase.MoveAsset(a.path, newPath);
affectedFolders.Add(Path.GetDirectoryName(a.path));
break;
/*case OrganizeAction.Copy:
AssetDatabase.CopyAsset(a.path, newPath);
break;*/
}
}
}
finally
{
EditorUtility.ClearProgressBar();
AssetDatabase.StopAssetEditing();
}
try
{
AssetDatabase.StartAssetEditing();
foreach (var folderPath in createdFolders.Concat(affectedFolders).Distinct().Where(DirectoryIsEmpty))
AssetDatabase.DeleteAsset(folderPath);
}
finally { AssetDatabase.StopAssetEditing(); }
EditorGUIUtility.PingObject(AssetDatabase.LoadMainAssetAtPath(destinationPath));
assets = null;
destinationPath = null;
}
#endregion
#region GUI Methods
private void DrawOrganizerTab()
{
GUIStyle boxStyle = GUI.skin.GetStyle("box");
using (new GUILayout.HorizontalScope())
{
EditorGUI.BeginChangeCheck();
mainAsset = EditorGUILayout.ObjectField("Main Asset", mainAsset, typeof(Object), false);
if (EditorGUI.EndChangeCheck())
{
if (mainAsset)
{
destinationPath = AssetDatabase.GetAssetPath(mainAsset);
if (!AssetDatabase.IsValidFolder(destinationPath)) destinationPath = Path.GetDirectoryName(destinationPath).Replace('\\', '/');
}
assets = null;
}
using (new EditorGUI.DisabledScope(!mainAsset))
if (GUILayout.Button("Get Assets", GUILayout.Width(80)))
GetDependencyAssets();
}
destinationPath = AssetFolderPath(destinationPath, "Destination Folder");
if (assets != null)
{
DrawSeparator(4, 20);
var h = EditorGUIUtility.singleLineHeight;
var squareOptions = new GUILayoutOption[] { GUILayout.Width(h), GUILayout.Height(h) };
foreach (var a in assets)
{
using (new GUILayout.HorizontalScope(boxStyle))
{
GUILayout.Label(a.icon, squareOptions);
if (GUILayout.Button($"| {a.path}", GUI.skin.label))
EditorGUIUtility.PingObject(a.asset);
a.action = (OrganizeAction)EditorGUILayout.EnumPopup(a.action, GUILayout.Width(60));
}
}
if (GUILayout.Button("Organize Assets"))
OrganizeAssets();
}
}
private void DrawOptionsTab()
{
optionsToolbarIndex = GUILayout.Toolbar(optionsToolbarIndex, optionTabs);
switch (optionsToolbarIndex)
{
case 0:
DrawFolderOptions();
break;
case 1:
DrawTypeOptions();
break;
}
DrawSeparator();
using (new GUILayout.HorizontalScope("helpbox"))
{
deleteEmptyFolders = EditorGUILayout.Toggle(new GUIContent("Delete Empty Folders", "After moving assets, delete source folders if they're empty"), deleteEmptyFolders);
sortByOption = (SortOptions)EditorGUILayout.EnumPopup("Sort Search By", sortByOption);
}
}
private void DrawFolderOptions()
{
for (var i = 0; i < specialFolders.Count; i++)
{
var f = specialFolders[i];
using (new GUILayout.HorizontalScope("helpbox"))
{
using (new BGColoredScope(Color.green, Color.grey, f.active))
f.active = GUILayout.Toggle(f.active, f.active ? "Enabled" : "Disabled", GUI.skin.button, GUILayout.Width(100), GUILayout.Height(18));
using (new EditorGUI.DisabledScope(!f.active))
{
f.name = GUILayout.TextField(f.name);
f.action = (OrganizeAction) EditorGUILayout.EnumPopup(f.action, GUILayout.Width(60));
if (GUILayout.Button("X", GUILayout.Width(18), GUILayout.Height(18)))
specialFolders.RemoveAt(i);
}
}
}
if (GUILayout.Button("Add"))
specialFolders.Add(new CustomFolder());
}
private void DrawTypeOptions()
{
using (new GUILayout.HorizontalScope())
{
void DrawTypeGUI(OrganizeType t)
{
var icon = GUIContent.none;
if (t.associatedTypes.Length > 0)
icon = new GUIContent(AssetPreview.GetMiniTypeThumbnail(t.associatedTypes[0]));
using (new GUILayout.HorizontalScope())
{
GUILayout.Label(icon, GUILayout.Height(18), GUILayout.Width(18));
GUILayout.Label($"| {t.name}");
if (TryGetTypeAction(t, out _))
typeActions[t.actionIndex] = (OrganizeAction)EditorGUILayout.EnumPopup(typeActions[t.actionIndex], GUILayout.Width(60));
}
}
using (new GUILayout.VerticalScope("helpbox"))
{
for (int i = 0; i < organizeTypes.Length; i+=2)
DrawTypeGUI(organizeTypes[i]);
}
using (new GUILayout.VerticalScope("helpbox"))
{
for (int i = 1; i < organizeTypes.Length; i += 2)
DrawTypeGUI(organizeTypes[i]);
}
}
}
private static string AssetFolderPath(string variable, string title)
{
using (new GUILayout.HorizontalScope())
{
EditorGUILayout.TextField(title, variable);
if (GUILayout.Button("...", GUILayout.Width(30)))
{
var dummyPath = EditorUtility.OpenFolderPanel(title, AssetDatabase.IsValidFolder(variable) ? variable : "Assets", string.Empty);
if (string.IsNullOrEmpty(dummyPath))
return variable;
string newPath = FileUtil.GetProjectRelativePath(dummyPath);
if (!newPath.StartsWith("Assets"))
{
Debug.LogWarning("New Path must be a folder within Assets!");
return variable;
}
variable = newPath;
}
}
return variable;
}
private static void DrawSeparator(int thickness = 2, int padding = 10)
{
Rect r = EditorGUILayout.GetControlRect(GUILayout.Height(thickness + padding));
r.height = thickness;
r.y += padding / 2f;
r.x -= 2;
r.width += 6;
ColorUtility.TryParseHtmlString(EditorGUIUtility.isProSkin ? "#595959" : "#858585", out Color lineColor);
EditorGUI.DrawRect(r, lineColor);
}
#endregion
#region Sub-Main Methods
[MenuItem("DreadTools/Asset Organizer", false, 36)]
private static void ShowWindow() => GetWindow<AssetOrganizer>(false, "Asset Organizer", true);
private bool TrySetAction(DependencyAsset a)
{
for (int i = 0; i < organizeTypes.Length; i++)
{
if (!organizeTypes[i].IsAppliedTo(a)) continue;
if (TryGetTypeAction(organizeTypes[i], out var action))
{
a.action = action;
a.associatedType = organizeTypes[i];
return true;
}
}
return false;
}
private bool TryGetTypeAction(OrganizeType type, out OrganizeAction action)
{
bool hasDoubleTried = false;
TryAgain:
try
{
action = typeActions[type.actionIndex];
return true;
}
catch (Exception)
{
if (hasDoubleTried) throw;
OrganizeAction[] newArray = new OrganizeAction[organizeTypes.Length];
for (int j = 0; j < newArray.Length; j++)
{
try { newArray[j] = typeActions[j]; }
catch { newArray[j] = OrganizeAction.Skip; }
}
Debug.LogWarning("Type Actions re-initialized due to a loading/serialization.");
typeActions = newArray;
hasDoubleTried = true;
goto TryAgain;
}
}
private static void CheckFolders()
{
if (!destinationPath.StartsWith("Assets/"))
destinationPath = "Assets/" + destinationPath;
ReadyPath(destinationPath);
createdFolders.Clear();
void CheckFolder(string name)
{
string path = $"{destinationPath}/{name}";
if (ReadyPath(path)) createdFolders.Add(path);
}
try
{
AssetDatabase.StartAssetEditing();
for (int i = 0; i < organizeTypes.Length; i++)
CheckFolder(organizeTypes[i].name);
}
finally { AssetDatabase.StopAssetEditing(); }
}
public static void DeleteIfEmptyFolder(string folderPath)
{
if (!AssetDatabase.IsValidFolder(folderPath))
folderPath = Path.GetDirectoryName(folderPath);
while (DirectoryIsEmpty(folderPath) && folderPath != "Assets")
{
var parentDirectory = Path.GetDirectoryName(folderPath);
FileUtil.DeleteFileOrDirectory(folderPath);
FileUtil.DeleteFileOrDirectory(folderPath + ".meta");
folderPath = parentDirectory;
}
}
public static bool DirectoryIsEmpty(string path) => !Directory.EnumerateFileSystemEntries(path).Any();
#endregion
#region Automated Methods
private void OnEnable()
{
string data = EditorPrefs.GetString(PrefsKey, JsonUtility.ToJson(this, false));
JsonUtility.FromJsonOverwrite(data, this);
if (!EditorPrefs.HasKey(PrefsKey))
{
//Default Folder based actions. Based on usual VRC assets.
specialFolders = new List<CustomFolder>
{
new CustomFolder("VRCSDK"),
new CustomFolder("_PoiyomiShaders"),
new CustomFolder("VRLabs"),
new CustomFolder("PumkinsAvatarTools"),
new CustomFolder("DreadScripts"),
new CustomFolder("Packages"),
new CustomFolder("Plugins"),
new CustomFolder("Editor")
};
//Default Type based Actions
typeActions = new OrganizeAction[]
{
OrganizeAction.Move,
OrganizeAction.Move,
OrganizeAction.Move,
OrganizeAction.Move,
OrganizeAction.Move,
OrganizeAction.Move,
OrganizeAction.Move,
OrganizeAction.Move,
OrganizeAction.Move,
OrganizeAction.Skip,
OrganizeAction.Move,
OrganizeAction.Skip,
OrganizeAction.Skip,
OrganizeAction.Skip,
};
}
createdFolders = new List<string>();
}
private void OnDisable()
{
string data = JsonUtility.ToJson(this, false);
EditorPrefs.SetString(PrefsKey, data);
}
#endregion
#region Helper Methods
private static void Credit()
{
using (new GUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
if (GUILayout.Button("Made By Dreadrith#3238", "boldlabel"))
Application.OpenURL("https://linktr.ee/Dreadrith");
}
}
private static bool ReadyPath(string folderPath)
{
if (Directory.Exists(folderPath)) return false;
Directory.CreateDirectory(folderPath);
AssetDatabase.ImportAsset(folderPath);
return true;
}
public static List<string> GetAssetPathsInFolder(string path, bool deep = true)
{
string[] fileEntries = Directory.GetFiles(path);
string[] subDirectories = deep ? AssetDatabase.GetSubFolders(path) : null;
List<string> list =
(from fileName in fileEntries
where !fileName.EndsWith(".meta")
select fileName.Replace('\\', '/')).ToList();
if (deep)
foreach (var sd in subDirectories)
list.AddRange(GetAssetPathsInFolder(sd));
return list;
}
#endregion
#endregion
#region Classes & Structs
[System.Serializable]
private class CustomFolder
{
public string name;
public bool active = true;
public OrganizeAction action;
public CustomFolder(){}
public CustomFolder(string newName, OrganizeAction action = OrganizeAction.Skip)
{
name = newName;
this.action = action;
}
}
private class DependencyAsset
{
public readonly Object asset;
public readonly string path;
public readonly Type type;
public readonly GUIContent icon;
public OrganizeAction action;
public OrganizeType associatedType;
public DependencyAsset(string path)
{
this.path = path;
asset = AssetDatabase.LoadAssetAtPath<Object>(path);
action = OrganizeAction.Skip;
type = asset.GetType();
icon = new GUIContent(AssetPreview.GetMiniTypeThumbnail(type), type.Name);
}
}
private readonly struct OrganizeType
{
public readonly int actionIndex;
public readonly string name;
public readonly Type[] associatedTypes;
private readonly string[] associatedExtensions;
public OrganizeType(int actionIndex, string name)
{
this.actionIndex = actionIndex;
this.name = name;
this.associatedTypes = Array.Empty<Type>();
this.associatedExtensions = Array.Empty<string>();
}
public OrganizeType(int actionIndex, string name, params string[] associatedTypes)
{
this.actionIndex = actionIndex;
this.name = name;
this.associatedTypes = new Type[associatedTypes.Length];
for (int i = 0; i < associatedTypes.Length; i++)
this.associatedTypes[i] = System.Type.GetType(associatedTypes[i]);
this.associatedExtensions = Array.Empty<string>();
}
public OrganizeType(int actionIndex, string name, params Type[] associatedTypes)
{
this.actionIndex = actionIndex;
this.name = name;
this.associatedTypes = associatedTypes;
this.associatedExtensions = Array.Empty<string>();
}
public OrganizeType(int actionIndex, string name, string[] associatedExtensions, params string[] associatedTypes)
{
this.actionIndex = actionIndex;
this.name = name;
this.associatedTypes = new Type[associatedTypes.Length];
for (int i = 0; i < associatedTypes.Length; i++)
this.associatedTypes[i] = System.Type.GetType(associatedTypes[i]);
this.associatedExtensions = associatedExtensions;
}
public OrganizeType(int actionIndex, string name, string[] associatedExtensions, params Type[] associatedTypes)
{
this.actionIndex = actionIndex;
this.name = name;
int count = associatedTypes.Length;
this.associatedTypes = associatedTypes;
this.associatedExtensions = associatedExtensions;
}
public bool IsAppliedTo(DependencyAsset a)
{
bool applies = a.type != null &&
(associatedTypes.Any(t => t != null && (a.type == t || a.type.IsSubclassOf(t)))
|| associatedExtensions.Any(e => !string.IsNullOrWhiteSpace(e) && a.path.EndsWith(e)));
return applies;
}
}
private class BGColoredScope : System.IDisposable
{
private readonly Color ogColor;
public BGColoredScope(Color setColor)
{
ogColor = GUI.backgroundColor;
GUI.backgroundColor = setColor;
}
public BGColoredScope(Color setColor, bool isActive)
{
ogColor = GUI.backgroundColor;
GUI.backgroundColor = isActive ? setColor : ogColor;
}
public BGColoredScope(Color active, Color inactive, bool isActive)
{
ogColor = GUI.backgroundColor;
GUI.backgroundColor = isActive ? active : inactive;
}
public BGColoredScope(int selectedIndex, params Color[] colors)
{
ogColor = GUI.backgroundColor;
GUI.backgroundColor = colors[selectedIndex];
}
public void Dispose()
{
GUI.backgroundColor = ogColor;
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c3350e00f0c1769488b502a2a4f77a48
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
{
"name": "com.dreadscripts.assetorganizer.Editor",
"references": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ffecb479d42e80945a3b21adb02b71e4
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

21
AssetOrganizer/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Dreadrith
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9587cb3598e7645408cf231bd10e0beb
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

22
AssetOrganizer/README.md Normal file
View File

@@ -0,0 +1,22 @@
# Asset Organizer
Reorganizes Prefab, Scene or Folder's dependency assets into their own sorted folders
### [Download From Here](https://vpm.dreadscripts.com/)
## Features
- Organize assets in your project based on their types or custom folders.
- Define custom folder actions for specific asset types.
- Choose from various sorting options to manage your assets effectively.
- Delete empty folders automatically after organizing assets.
## How to Use
1. Open the Unity Editor.
2. Navigate to `DreadTools > Asset Organizer` in the top menu.
3. Choose the "Organizer" tab to organize assets based on their types or custom folders.
4. Use the "Options" tab to configure folder actions, type actions, and sorting options.
5. Click "Get Assets" to select a main asset and gather its dependencies.
6. Adjust the organization settings as needed.
7. Click "Organize Assets" to apply the changes.
### Thank You
If you enjoy Asset Organizer, please consider [supporting me ♡](https://ko-fi.com/Dreadrith)!

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 293a57234d624b74ab4a7fbe07d0ac19
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
{
"name": "com.dreadscripts.assetorganizer",
"displayName": "DreadScripts - AssetOrganizer",
"version": "1.0.1",
"description": "Reorganizes Prefab, Scene or Folder's dependency assets into their own sorted folders.",
"gitDependencies": {},
"vpmDependencies": {},
"author": {
"name": "Dreadrith",
"email": "dreadscripts@gmail.com",
"url": "https://www.dreadrith.com"
},
"legacyFolders": {},
"legacyFiles": {},
"type": "tool",
"unity": "2019.4"
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e4bc2cfdad5430543a82759af2a714dc
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: