Skip to content

Instantly share code, notes, and snippets.

@noio
Created October 29, 2024 07:50
Show Gist options
  • Save noio/d118eb047adb19ad63b5a93b4da1ba32 to your computer and use it in GitHub Desktop.
Save noio/d118eb047adb19ad63b5a93b4da1ba32 to your computer and use it in GitHub Desktop.
// (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
}
}
// (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