-
-
Save noio/d118eb047adb19ad63b5a93b4da1ba32 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// (C)2024 @noio_games | |
// Thomas van den Berg | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using TMPro; | |
using UnityEditor; | |
using UnityEngine; | |
using Object = UnityEngine.Object; | |
namespace Utils.AssetOrganizer | |
{ | |
[ExecuteAlways] | |
public class Organizer : MonoBehaviour | |
{ | |
#region SERIALIZED FIELDS | |
[SerializeField] Color _color = new(1, 1, 0, 1); | |
[SerializeField] TMP_Text _label; | |
[SerializeField] MeshRenderer _border; | |
[SerializeField] SortKey _sortBy; | |
[SerializeField] SortKey _sortBySecondary = SortKey.Name; | |
[SerializeField] float _maxRowLength = 10f; | |
[SerializeField] Vector2 _spacing = new(1f, 1f); // x = horizontal, y = depth spacing | |
[SerializeField] Vector2 _padding = new(1f, 1f); // x = horizontal, y = depth spacing | |
[SerializeField] bool _raiseChildrenIfUnderground = true; | |
[HideInInspector] [SerializeField] Bounds _bounds; | |
[SerializeField] Material _gizmoMaterial; | |
#endregion | |
#region PROPERTIES | |
public float MaxRowLength | |
{ | |
get => _maxRowLength; | |
set => _maxRowLength = value; | |
} | |
#endregion | |
#region MONOBEHAVIOUR METHODS | |
void OnEnable() | |
{ | |
UpdateBorder(); | |
} | |
#endregion | |
public enum SortKey | |
{ | |
None, | |
AssetPath, | |
Name, | |
SizeZ | |
} | |
#if UNITY_EDITOR | |
readonly Lazy<Mesh> _defaultCubeMesh = new(() => Resources.GetBuiltinResource<Mesh>("Cube.fbx")); | |
static readonly int OutlineColorID = Shader.PropertyToID("_OutlineColor"); | |
static MaterialPropertyBlock _propertyBlock; | |
// public static void SendUpdateOrganizer(Component component) | |
// { | |
// if (component != null) | |
// { | |
// component.SendMessageUpwards(nameof(OnOrganize), SendMessageOptions.DontRequireReceiver); | |
// } | |
// } | |
// [UsedImplicitly] | |
// void OnOrganize() | |
// { | |
// DoOrganize(); | |
// } | |
void OnDrawGizmos() | |
{ | |
var sizeAndOffset = Matrix4x4.TRS(_bounds.center, Quaternion.identity, _bounds.size); | |
var matrix = transform.localToWorldMatrix * sizeAndOffset; | |
// _gizmoMaterial.SetColor(OutlineColorID, _color); | |
// _gizmoMaterial.SetPass(0); | |
// Graphics.DrawMeshNow(_defaultCubeMesh.Value, matrix, 0); | |
/* | |
* Use regular gizmo to make clickable | |
*/ | |
var biggerBounds = _bounds; | |
var size = biggerBounds.size; | |
size.y = .4f; | |
size.z += 10; // stretch to include label | |
biggerBounds.size = size; | |
Gizmos.matrix = transform.localToWorldMatrix; | |
Gizmos.color = new Color(0f, 0f, 0f, 0.01f); | |
Gizmos.DrawCube(biggerBounds.center, biggerBounds.size); | |
} | |
void OnDrawGizmosSelected() | |
{ | |
/* | |
* Use regular gizmo to make clickable | |
*/ | |
var size = _bounds.size; | |
_bounds.size = size; | |
Gizmos.matrix = transform.localToWorldMatrix; | |
Gizmos.color = new Color(0.26f, 0.91f, 0.06f, 0.73f); | |
Gizmos.DrawWireCube(_bounds.center, _bounds.size); | |
} | |
public static void TriggerUpdatesFor(Object[] objects) | |
{ | |
/* | |
* First find the UNIQUE 'root' organizer objects (highest in the hierarchy) | |
* then find ALL their child organizers | |
* then update BOTTOM UP | |
*/ | |
var roots = new List<Organizer>(); | |
foreach (var ob in objects) | |
{ | |
if (TryFindRootOrganizer(ob, out var rootOrganizer)) | |
{ | |
if (roots.Contains(rootOrganizer) == false) | |
{ | |
roots.Add(rootOrganizer); | |
} | |
} | |
} | |
/* | |
* For each root, update the hierarchy BOTTOM UP | |
*/ | |
foreach (var root in roots) | |
{ | |
var hierarchy = new List<Organizer>(); | |
root.GetComponentsInChildren(hierarchy); | |
hierarchy.Reverse(); | |
foreach (var organizer in hierarchy) | |
{ | |
organizer.DoOrganize(); | |
} | |
} | |
return; | |
static bool TryFindRootOrganizer(Object ob, out Organizer rootOrganizer) | |
{ | |
var component = (Component)ob; | |
var transform = component.transform; | |
rootOrganizer = null; | |
while (transform != null) | |
{ | |
/* | |
* If we find a new component, overwrite field: | |
*/ | |
rootOrganizer = transform.GetComponent<Organizer>() ?? rootOrganizer; | |
transform = transform.parent; | |
} | |
return rootOrganizer != null; | |
} | |
} | |
public void DoOrganize() | |
{ | |
// Register undo | |
Undo.RecordObject(transform, "Align Objects"); | |
Undo.RecordObject(this, "Align Objects"); | |
// Get all direct children and their bounds | |
var children = new List<(Transform child, Bounds bounds)>(); | |
for (var i = 0; i < transform.childCount; i++) | |
{ | |
var child = transform.GetChild(i); | |
if (child.gameObject.activeInHierarchy == false || | |
(_label != null && child == _label.transform) || | |
(_border != null && child == _border.transform)) | |
{ | |
continue; | |
} | |
/* | |
* Nested Organizers use those bounds | |
*/ | |
if (child.TryGetComponent(out Organizer organizer)) | |
{ | |
children.Add((child, organizer._bounds)); | |
} | |
else | |
{ | |
var renderers = new List<Renderer>(); | |
renderers.AddRange(child.GetComponentsInChildren<MeshRenderer>()); | |
renderers.AddRange(child.GetComponentsInChildren<SkinnedMeshRenderer>()); | |
if (GameObjectUtils.TryGetLocalBounds(renderers, child.worldToLocalMatrix, out var bounds)) | |
{ | |
children.Add((child, bounds)); | |
} | |
} | |
} | |
if (_sortBy != SortKey.None) | |
{ | |
children = children.OrderBy(GetSortFunc(_sortBy)) | |
.ThenBy(GetSortFunc(_sortBySecondary)) | |
.ToList(); | |
} | |
var nextPosition = new Vector3(_padding.x, 0, _padding.y); | |
Vector3 max = default; | |
for (var i = 0; i < children.Count; i++) | |
{ | |
var (child, localBounds) = children[i]; | |
child.SetAsLastSibling(); | |
// Calculate target position accounting for bounds center | |
var targetPos = GetTargetPos(nextPosition, localBounds, out var objectEnd); | |
// DebugDraw.WireSphere(transform.TransformPoint(nextPosition), .2f, Color.green, 10); | |
// Check if we need to start a new row | |
if (i > 0 && objectEnd.x > _maxRowLength) | |
{ | |
// Move to next row | |
nextPosition.x = _padding.x; | |
nextPosition.z = max.z + _spacing.y; | |
/* | |
* Get targetPos on new row | |
*/ | |
targetPos = GetTargetPos(nextPosition, localBounds, out objectEnd); | |
} | |
if (_raiseChildrenIfUnderground) | |
{ | |
targetPos.y = Mathf.Max(0, -Mathf.Round(localBounds.min.y)); | |
} | |
// Move object to position | |
child.localPosition = targetPos; | |
child.localRotation = Quaternion.identity; | |
max = Vector3.Max(max, objectEnd); | |
// DebugDraw.WireCube(child.TransformPoint(localBounds.center), localBounds.extents, child.rotation, | |
// Color.red, 10); | |
// Update next position using the right edge of the bounds | |
nextPosition.x = objectEnd.x + _spacing.x; | |
} | |
max.x += _padding.x; | |
max.z += _padding.y; | |
_bounds = new Bounds(max * .5f, max); | |
UpdateLabel(); | |
UpdateBorder(); | |
return; | |
static Vector3 GetTargetPos(Vector3 nextPosition, Bounds localBounds, out Vector3 objectEnd) | |
{ | |
Vector3 targetPos = default; | |
targetPos.x = Mathf.Ceil(nextPosition.x - localBounds.min.x); | |
targetPos.z = Mathf.Ceil(nextPosition.z - localBounds.min.z); | |
objectEnd = targetPos + localBounds.max; | |
return targetPos; | |
} | |
} | |
static Func<(Transform child, Bounds bounds), IComparable> GetSortFunc(SortKey sortKey) | |
{ | |
return tuple => | |
{ | |
var (child, bounds) = tuple; | |
return sortKey switch | |
{ | |
SortKey.None => 0, | |
SortKey.Name => child.name, | |
SortKey.SizeZ => Mathf.Round(bounds.size.z), | |
SortKey.AssetPath => AssetDatabase.GetAssetPath( | |
PrefabUtility.GetCorrespondingObjectFromSource(child)), | |
_ => 0 | |
}; | |
}; | |
} | |
public void UpdateLabel() | |
{ | |
if (_label != null) | |
{ | |
_label.name = "LABEL"; | |
_label.transform.SetAsFirstSibling(); | |
_label.text = name; | |
_label.fontSize = 24; | |
_label.font = TMP_Settings.defaultFontAsset; | |
_label.characterSpacing = -8.5f; | |
_label.fontStyle = FontStyles.Bold | FontStyles.UpperCase; | |
_label.enableAutoSizing = true; | |
_label.fontSizeMax = 24; | |
var labelPos = _bounds.min; | |
labelPos.x = _bounds.center.x; | |
labelPos.y += .05f; | |
labelPos.z -= .5f; | |
var col = _color; | |
col.a = 1; | |
_label.color = col; | |
_label.transform.localPosition = labelPos; | |
_label.transform.localRotation = Quaternion.Euler(90, 0, 0); | |
// var folderName = Path.GetFileName((_folders.Select(f => f.Path).FirstOrDefault())); | |
// if (folderName != null) | |
_label.text = name; | |
_label.rectTransform.pivot = new Vector2(.5f, 1); | |
_label.rectTransform.sizeDelta = | |
new Vector2(Mathf.Min(20, _bounds.size.x), _label.textBounds.size.y); | |
} | |
} | |
public void UpdateBorder() | |
{ | |
if (_border != null) | |
{ | |
var borderTransform = _border.transform; | |
borderTransform.SetAsFirstSibling(); | |
var pos = _bounds.center; | |
pos.y = .05f; | |
borderTransform.localPosition = pos; | |
var size = _bounds.size; | |
size.y = .1f; | |
borderTransform.localScale = size; | |
_propertyBlock ??= new MaterialPropertyBlock(); | |
_propertyBlock.Clear(); | |
// var sizeAndOffset = Matrix4x4.TRS(bounds.center, Quaternion.identity, bounds.size); | |
// var matrix = transform.localToWorldMatrix * sizeAndOffset; | |
_propertyBlock.SetColor(OutlineColorID, _color); | |
_border.SetPropertyBlock(_propertyBlock); | |
} | |
// _gizmoMaterial.SetPass(0); | |
// Graphics.DrawMeshNow(_defaultCubeMesh.Value, matrix, 0); | |
// /* | |
// * Use regular gizmo to make clickable | |
// */ | |
// var size = bounds.size; | |
// size.y = .4f; | |
// size.z += 10; // stretch to include label | |
// bounds.size = size; | |
// Gizmos.matrix = transform.localToWorldMatrix; | |
// Gizmos.color = new Color(0f, 0f, 0f, 0.01f); | |
// Gizmos.DrawCube(bounds.center, bounds.size); | |
// } | |
} | |
#endif | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// (C)2024 @noio_games | |
// Thomas van den Berg | |
using System; | |
using TMPro; | |
using UnityEditor; | |
using UnityEngine; | |
using Utils.Editor; | |
namespace Utils.AssetOrganizer.Editor | |
{ | |
[CanEditMultipleObjects] | |
[CustomEditor(typeof(Organizer))] | |
public class OrganizerInspector : UnityEditor.Editor | |
{ | |
static readonly Lazy<GUIContent> RebuildButtonContent = new(() => | |
new GUIContent(EditorGUIUtility.IconContent("d_Refresh")) | |
{ | |
text = " Sort & Align", | |
tooltip = "Add All Prefabs and Sort on Grid." | |
}); | |
static readonly Lazy<Mesh> DefaultCubeMesh = new(() => Resources.GetBuiltinResource<Mesh>("Cube.fbx")); | |
static readonly Lazy<Material> DefaultBorderMaterial = new(() => | |
AssetDatabase.LoadAssetAtPath<Material>( | |
AssetDatabase.GUIDToAssetPath("3ea7e780144e649948eea9c408ac5d81"))); | |
SerializedProperty _colorProp; | |
SerializedProperty _labelProp; | |
SerializedProperty _borderProp; | |
SerializedProperty _maxRowLengthProp; | |
SerializedProperty _spacingProp; | |
SerializedProperty _paddingProp; | |
SerializedProperty _raiseChildrenIfUndergroundProp; | |
SerializedProperty _sortByProp; | |
SerializedProperty _sortBySecondaryProp; | |
// Hidden; | |
SerializedProperty _boundsProp; | |
SerializedProperty _gizmoMaterialProp; | |
#region MONOBEHAVIOUR METHODS | |
void OnEnable() | |
{ | |
EditorUtils.InitializeProperties(this, serializedObject); | |
} | |
#endregion | |
public override void OnInspectorGUI() | |
{ | |
serializedObject.Update(); | |
EditorUtils.DrawScriptProp(serializedObject); | |
// DrawDefaultInspector(); | |
using (var changes = new EditorGUI.ChangeCheckScope()) | |
{ | |
EditorGUILayout.PropertyField(_colorProp); | |
using (new GUILayout.HorizontalScope()) | |
{ | |
EditorGUILayout.PropertyField(_labelProp); | |
if (serializedObject.isEditingMultipleObjects == false) | |
{ | |
if (_labelProp.objectReferenceValue == null) | |
{ | |
if (GUILayout.Button("Create")) | |
{ | |
CreateLabel(); | |
} | |
} | |
} | |
} | |
using (new GUILayout.HorizontalScope()) | |
{ | |
EditorGUILayout.PropertyField(_borderProp); | |
if (serializedObject.isEditingMultipleObjects == false) | |
{ | |
if (_borderProp.objectReferenceValue == null) | |
{ | |
if (GUILayout.Button("Create")) | |
{ | |
CreateBorder(); | |
} | |
} | |
} | |
} | |
if (changes.changed) | |
{ | |
OnEach(gs => gs.UpdateLabel()); | |
} | |
} | |
EditorGUILayout.PropertyField(_gizmoMaterialProp); | |
EditorGUI.BeginChangeCheck(); | |
using (new SectionScope("Hierarchy")) | |
{ | |
if (GUILayout.Button("Move Up")) | |
{ | |
var transform = ((Organizer)target).transform; | |
transform.SetSiblingIndex(transform.GetSiblingIndex() - 1); | |
} | |
if (GUILayout.Button("Move Down")) | |
{ | |
var transform = ((Organizer)target).transform; | |
transform.SetSiblingIndex(transform.GetSiblingIndex() + 1); | |
} | |
} | |
using (new SectionScope("Sorting")) | |
{ | |
EditorGUILayout.PropertyField(_sortByProp); | |
var primarySort = (Organizer.SortKey)_sortByProp.intValue; | |
using (new EditorGUI.DisabledScope(primarySort == Organizer.SortKey.None)) | |
{ | |
EditorGUILayout.PropertyField(_sortBySecondaryProp); | |
} | |
} | |
using (new SectionScope("Align on Grid")) | |
{ | |
EditorGUILayout.PropertyField(_maxRowLengthProp); | |
EditorGUILayout.PropertyField(_spacingProp); | |
EditorGUILayout.PropertyField(_paddingProp); | |
EditorGUILayout.PropertyField(_raiseChildrenIfUndergroundProp); | |
} | |
if (EditorGUI.EndChangeCheck()) | |
{ | |
serializedObject.ApplyModifiedProperties(); | |
OnEach(gs => { gs.DoOrganize(); }); | |
Debouncer.Do(OrganizeHierarchy); | |
} | |
if (GUILayout.Button(RebuildButtonContent.Value, GUILayout.Height(30))) | |
{ | |
OnEach(gs => { gs.DoOrganize(); }); | |
Debouncer.Do(OrganizeHierarchy); | |
serializedObject.Update(); | |
} | |
using (new EditorGUI.DisabledScope(true)) | |
{ | |
EditorGUILayout.PropertyField(_boundsProp); | |
} | |
serializedObject.ApplyModifiedProperties(); | |
} | |
void CreateLabel() | |
{ | |
var organizer = (Organizer)target; | |
var label = new GameObject("Label"); | |
Undo.RegisterCreatedObjectUndo(label, "Create Label"); | |
label.transform.SetParent(organizer.transform); | |
label.AddComponent<MeshRenderer>(); | |
var text = label.AddComponent<TextMeshPro>(); | |
_labelProp.objectReferenceValue = text; | |
serializedObject.ApplyModifiedProperties(); | |
organizer.UpdateLabel(); | |
} | |
void CreateBorder() | |
{ | |
var organizer = (Organizer)target; | |
var border = new GameObject("BORDER"); | |
Undo.RegisterCreatedObjectUndo(border, "Add Border"); | |
border.transform.SetParent(organizer.transform); | |
var meshFilter = border.AddComponent<MeshFilter>(); | |
meshFilter.sharedMesh = DefaultCubeMesh.Value; | |
var meshRenderer = border.AddComponent<MeshRenderer>(); | |
meshRenderer.sharedMaterial = DefaultBorderMaterial.Value; | |
_borderProp.objectReferenceValue = meshRenderer; | |
serializedObject.ApplyModifiedProperties(); | |
organizer.UpdateBorder(); | |
} | |
void OrganizeHierarchy() | |
{ | |
Organizer.TriggerUpdatesFor(targets); | |
// Organizer.SendUpdateOrganizer(organizer.transform.parent); | |
} | |
void OnSceneGUI() | |
{ | |
var organizer = (Organizer)target; | |
var transform = organizer.transform; | |
// Convert max row length to world space for handle | |
var handlePos = transform.TransformPoint(new Vector3(organizer.MaxRowLength, 0, 0)); | |
// Draw line to show current width | |
Handles.color = Color.white; | |
Handles.DrawLine(transform.position, handlePos); | |
// Create a slider handle | |
EditorGUI.BeginChangeCheck(); | |
var newHandlePos = Handles.Slider( | |
handlePos, | |
transform.right, // Constrain to X axis in local space | |
1.1f, | |
Handles.DotHandleCap, | |
1f // Snap value | |
); | |
if (EditorGUI.EndChangeCheck()) | |
{ | |
// Convert world position back to local space | |
var localPos = transform.InverseTransformPoint(newHandlePos); | |
// Record undo | |
Undo.RecordObject(target, "Change Max Row Length"); | |
// Update the serialized property | |
organizer.MaxRowLength = Mathf.Max(1f, Mathf.Ceil(localPos.x)); | |
// serializedObject.ApplyModifiedProperties(); | |
// Update grid if needed | |
organizer.DoOrganize(); | |
Debouncer.Do(OrganizeHierarchy); | |
} | |
} | |
void OnEach(Action<Organizer> action) | |
{ | |
foreach (var t in targets) | |
{ | |
if (t is Organizer gridSorter) | |
{ | |
action(gridSorter); | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment