From a868cc74ac4caadb4d7c16c400a1f6d0871468f0 Mon Sep 17 00:00:00 2001 From: originalnicodr Date: Sat, 27 Jul 2024 13:24:52 -0300 Subject: [PATCH] Refactored bone panel to mimic a hierarchical-like structure, so the main bones are displayed first, and you can get more granular control by expanding their children. --- src/Cinematic/Cells/BonesCell.cs | 76 ++++++++++++--- src/UI/Panels/BonesPanel.cs | 158 +++++++++++++++++++++++++++++-- 2 files changed, 217 insertions(+), 17 deletions(-) diff --git a/src/Cinematic/Cells/BonesCell.cs b/src/Cinematic/Cells/BonesCell.cs index 26357ec..99edeef 100644 --- a/src/Cinematic/Cells/BonesCell.cs +++ b/src/Cinematic/Cells/BonesCell.cs @@ -10,6 +10,7 @@ namespace UnityExplorer.UI.Panels { public Transform bone; public TransformControls transformControls; + private BoneTree boneTree; ComponentControl positionControl; ComponentControl rotationControl; @@ -17,6 +18,9 @@ namespace UnityExplorer.UI.Panels public AxisComponentControl CurrentSlidingAxisControl { get; set; } public BonesManager Owner; private ButtonRef inspectButton; + private ButtonRef expandBones; + private LayoutElement spaceLayout; + static private int TREE_LEVEL_IDENTATION = 20; // ICell public float DefaultHeight => 25f; @@ -27,20 +31,66 @@ namespace UnityExplorer.UI.Panels public void Enable() => UIRoot.SetActive(true); public void Disable() => UIRoot.SetActive(false); - public void SetBone(Transform bone, BonesManager bonesManager){ - this.bone = bone; - inspectButton.ButtonText.text = bone.name; + public void SetBoneTree(BoneTree boneTree, BonesManager bonesManager){ + this.boneTree = boneTree; + this.bone = boneTree.obj.transform; + inspectButton.ButtonText.text = boneTree.obj.name; Owner = bonesManager; + + if (boneTree.childTrees.Count == 0){ + expandBones.Component.gameObject.SetActive(false); + } else { + expandBones.Component.gameObject.SetActive(true); + expandBones.ButtonText.text = IsTreeExpanded() ? "▼" : "▶"; + } + + spaceLayout.minWidth = TREE_LEVEL_IDENTATION * boneTree.level; + } + + private void ExpandOrCollapseBoneTree(){ + if (IsTreeExpanded()){ + // Collapse + List treesToRemove = boneTree.childTrees.Select(t => t.flatten()).SelectMany(l => l).ToList(); + Owner.boneTrees = Owner.boneTrees.Except(treesToRemove).ToList(); + expandBones.ButtonText.text = "▶"; + } + else { + // Expand + int index = Owner.boneTrees.FindIndex(t => t == boneTree); + if (index == -1) return; + + Owner.boneTrees.InsertRange(index + 1, boneTree.childTrees); + expandBones.ButtonText.text = "▼"; + } + + Owner.boneScrollPool.Refresh(true, false); + } + + private bool IsTreeExpanded(){ + if (boneTree == null) return true; + return boneTree.childTrees.Any(t1 => Owner.boneTrees.Any(t2 => t2 == t1)); } public virtual GameObject CreateContent(GameObject parent) { - UIRoot = UIFactory.CreateUIObject("BoneCell", parent, new Vector2(25, 25)); - Rect = UIRoot.GetComponent(); - UIFactory.SetLayoutGroup(UIRoot, false, false, true, true, 3); - UIFactory.SetLayoutElement(UIRoot, minHeight: 25, minWidth: 50, flexibleWidth: 9999); + UIRoot = UIFactory.CreateUIObject("CellRoot", parent, new Vector2(25, 25)); + UIFactory.SetLayoutGroup(UIRoot, false, false, true, true, 4, childAlignment: TextAnchor.MiddleRight); + UIFactory.SetLayoutElement(UIRoot, minHeight: 25, flexibleWidth: 9999, flexibleHeight: 800); - GameObject header = UIFactory.CreateUIObject("BoneHeader", UIRoot); + Rect = UIRoot.GetComponent(); + Rect.anchorMin = new Vector2(0, 1); + Rect.anchorMax = new Vector2(0, 1); + Rect.pivot = new Vector2(0.5f, 1); + Rect.sizeDelta = new Vector2(30, 30); + + GameObject spacer = UIFactory.CreateUIObject("Spacer", UIRoot); + spaceLayout = UIFactory.SetLayoutElement(spacer, minWidth: 0, flexibleWidth: 0); + + GameObject baseCell = UIFactory.CreateUIObject("BaseCell", UIRoot); + UIFactory.SetLayoutGroup(baseCell, false, false, true, true, 3); + UIFactory.SetLayoutElement(baseCell, minHeight: 25, minWidth: 50, flexibleWidth: 9999); + + GameObject header = UIFactory.CreateUIObject("BoneHeader", baseCell); UIFactory.SetLayoutGroup(header, false, false, true, true, 4, childAlignment: TextAnchor.MiddleLeft); UIFactory.SetLayoutElement(header, minHeight: 25, flexibleWidth: 9999, flexibleHeight: 800); @@ -60,9 +110,13 @@ namespace UnityExplorer.UI.Panels UIFactory.SetLayoutElement(restoreBoneStateButton.GameObject, minWidth: 125, minHeight: 25); restoreBoneStateButton.OnClick += RestoreBoneState; - positionControl = ComponentControl.Create(this, UIRoot, "Local Position", TransformType.LocalPosition, 0.01f); - rotationControl = ComponentControl.Create(this, UIRoot, "Rotation", TransformType.Rotation, 10f); - scaleControl = ComponentControl.Create(this, UIRoot, "Scale", TransformType.Scale, 0.1f); + positionControl = ComponentControl.Create(this, baseCell, "Local Position", TransformType.LocalPosition, 0.01f); + rotationControl = ComponentControl.Create(this, baseCell, "Rotation", TransformType.Rotation, 10f); + scaleControl = ComponentControl.Create(this, baseCell, "Scale", TransformType.Scale, 0.1f); + + expandBones = UIFactory.CreateButton(baseCell, "ExpandBones", IsTreeExpanded() ? "⯆" : "▶", new Color(0.05f, 0.05f, 0.05f)); + UIFactory.SetLayoutElement(expandBones.Component.gameObject, minHeight: 25, minWidth: 25); + expandBones.OnClick += ExpandOrCollapseBoneTree; return UIRoot; } diff --git a/src/UI/Panels/BonesPanel.cs b/src/UI/Panels/BonesPanel.cs index a297a5c..45006ee 100644 --- a/src/UI/Panels/BonesPanel.cs +++ b/src/UI/Panels/BonesPanel.cs @@ -1,4 +1,5 @@ using UniverseLib.UI; +using UniverseLib.UI.Models; using UniverseLib.UI.Panels; using UniverseLib.UI.Widgets.ScrollView; @@ -18,8 +19,9 @@ namespace UnityExplorer.UI.Panels private List bones = new List(); private Dictionary bonesOriginalState = new(); - private ScrollPool boneScrollPool; - public int ItemCount => bones.Count; + public List boneTrees = new(); + public ScrollPool boneScrollPool; + public int ItemCount => boneTrees.Count; private bool DoneScrollPoolInit; public BonesManager(UIBase owner, List bones, IAnimator animator) : base(owner) @@ -27,6 +29,40 @@ namespace UnityExplorer.UI.Panels this.bones = bones; this.animator = animator; skeletonName.text = $"Skeleton: {animator?.name}"; + BuildBoneTrees(); + } + + private void BuildBoneTrees(){ + BoneTree root = new BoneTree(animator.wrappedObject.gameObject, bones); + if (root.obj != null){ + root.AssignLevels(); + boneTrees.Add(root); + } else { + foreach(BoneTree childTree in root.childTrees){ + childTree.AssignLevels(); + boneTrees.Add(childTree); + } + } + } + + private void CollapseBoneTrees(){ + boneTrees.Clear(); + BuildBoneTrees(); + boneScrollPool.Refresh(true, true); + } + + private void ExpandBoneTrees(){ + // We collapse before expanding to start from scratch and dont duplicate nodes + CollapseBoneTrees(); + + List newBoneTrees = new(); + + foreach(BoneTree childTree in boneTrees){ + newBoneTrees.AddRange(childTree.flatten()); + } + + boneTrees = newBoneTrees; + boneScrollPool.Refresh(true, false); } public override void SetActive(bool active) @@ -47,11 +83,23 @@ namespace UnityExplorer.UI.Panels UIFactory.SetLayoutElement(skeletonName.gameObject, minWidth: 100, minHeight: 25); skeletonName.fontSize = 16; - GameObject turnOffAnimatorToggleObj = UIFactory.CreateToggle(ContentRoot, "Animator toggle", out turnOffAnimatorToggle, out Text turnOffAnimatorToggleText); + GameObject header = UIFactory.CreateUIObject("Header", ContentRoot, new Vector2(25, 25)); + UIFactory.SetLayoutGroup(header, false, false, true, true, 4, childAlignment: TextAnchor.MiddleLeft); + UIFactory.SetLayoutElement(header, minHeight: 25, flexibleWidth: 9999, flexibleHeight: 800); + + GameObject turnOffAnimatorToggleObj = UIFactory.CreateToggle(header, "Animator toggle", out turnOffAnimatorToggle, out Text turnOffAnimatorToggleText); UIFactory.SetLayoutElement(turnOffAnimatorToggleObj, minHeight: 25, flexibleWidth: 9999); turnOffAnimatorToggle.onValueChanged.AddListener(OnTurnOffAnimatorToggle); turnOffAnimatorToggleText.text = "Toggle animator (needs to be off to move bones)"; + ButtonRef collapseAllButton = UIFactory.CreateButton(header, "CollapseAllButton", "Collapse all"); + UIFactory.SetLayoutElement(collapseAllButton.GameObject, minWidth: 150, minHeight: 25); + collapseAllButton.OnClick += CollapseBoneTrees; + + ButtonRef expandAllButton = UIFactory.CreateButton(header, "ExpandAllButton", "Expand all"); + UIFactory.SetLayoutElement(expandAllButton.GameObject, minWidth: 150, minHeight: 25); + expandAllButton.OnClick += ExpandBoneTrees; + boneScrollPool = UIFactory.CreateScrollPool(ContentRoot, "BonesList", out GameObject scrollObj, out GameObject scrollContent, new Color(0.06f, 0.06f, 0.06f)); UIFactory.SetLayoutElement(scrollObj, flexibleWidth: 9999, flexibleHeight: 9999); @@ -94,14 +142,14 @@ namespace UnityExplorer.UI.Panels public void SetCell(BonesCell cell, int index) { - if (index >= bones.Count) + if (index >= boneTrees.Count) { cell.Disable(); return; } - Transform bone = bones[index]; - cell.SetBone(bone, this); + BoneTree boneTree = boneTrees[index]; + cell.SetBoneTree(boneTree, this); cell.UpdateTransformControlValues(true); } @@ -132,4 +180,102 @@ namespace UnityExplorer.UI.Panels public readonly Vector3 angles; public readonly Vector3 scale; } + + public class BoneTree + { + public GameObject obj; + public int level; + public List childTrees = new(); + + public BoneTree(GameObject obj, List bones){ + // For some reason comparing GameObjects isn't working as intended in IL2CPP games, therefore we use their instance hash. +#if CPP + if (bones.Any(bone => bone.gameObject.GetInstanceID() == obj.GetInstanceID())) { + this.obj = obj; + } +#else + if (bones.Contains(obj.transform)) { + this.obj = obj; + } +#endif + for (int i = 0; i < obj.transform.childCount; i++) + { + Transform child = obj.transform.GetChild(i); + if (child.gameObject.activeSelf){ + childTrees.Add(new BoneTree(child.gameObject, bones)); + } + } + + Trim(); + childTrees = childTrees.OrderBy(b => b.obj.name).ToList(); + } + + private void Trim(){ + List newList = new(); + foreach (BoneTree childTree in childTrees) + { + if (childTree.obj == null){ + newList.AddRange(childTree.childTrees); + } else { + newList.Add(childTree); + } + } + + this.childTrees = newList; + } + + public void AssignLevels(){ + AssignLevel(0); + } + + private void AssignLevel(int distanceFromRoot){ + level = distanceFromRoot; + foreach (BoneTree childTree in childTrees) + { + childTree.AssignLevel(distanceFromRoot + 1); + } + } + + public override string ToString(){ + string return_string = ""; + if (obj != null){ + return_string = $"{obj.name} lvl: {level} - "; + } + + foreach (BoneTree childTree in childTrees) + { + return_string = return_string + childTree.ToString(); + } + + return return_string; + } + + public List getGameObjects(){ + List return_list = new(); + if (obj != null){ + return_list.Add(obj); + } + + foreach (BoneTree childTree in childTrees) + { + return_list.AddRange(childTree.getGameObjects()); + } + + return return_list; + } + + public List flatten(){ + List return_list = new(); + if (obj != null){ + return_list.Add(this); + } + + foreach (BoneTree childTree in childTrees) + { + return_list.AddRange(childTree.flatten()); + } + + return return_list; + } + } }