diff --git a/Packages/nadena.dev.modular-avatar/Editor/ClonedMenuMappings.cs b/Packages/nadena.dev.modular-avatar/Editor/ClonedMenuMappings.cs index 6f84950e..5b5a0694 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/ClonedMenuMappings.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/ClonedMenuMappings.cs @@ -3,21 +3,25 @@ using System.Collections.Immutable; using VRC.SDK3.Avatars.ScriptableObjects; // ReSharper disable once CheckNamespace -namespace nadena.dev.modular_avatar.core.editor { +namespace nadena.dev.modular_avatar.core.editor +{ public static class ClonedMenuMappings { - private static Dictionary> ClonedMappings = + private static readonly Dictionary> ClonedMappings = new Dictionary>(); - private static Dictionary OriginalMapping = + private static readonly Dictionary OriginalMapping = new Dictionary(); - public static void Clear() { + public static void Clear() + { ClonedMappings.Clear(); OriginalMapping.Clear(); } - public static void Add(VRCExpressionsMenu original, VRCExpressionsMenu clonedMenu) { - if (!ClonedMappings.TryGetValue(original, out ImmutableArray clonedMenus)) { + public static void Add(VRCExpressionsMenu original, VRCExpressionsMenu clonedMenu) + { + if (!ClonedMappings.TryGetValue(original, out ImmutableArray clonedMenus)) + { clonedMenus = ImmutableArray.Empty; } // Usually, one menu is rarely duplicated in multiple menus, so don't bother using a Builder @@ -25,11 +29,13 @@ namespace nadena.dev.modular_avatar.core.editor { OriginalMapping[clonedMenu] = original; } - public static bool TryGetClonedMenus(VRCExpressionsMenu original, out ImmutableArray clonedMenus) { + public static bool TryGetClonedMenus(VRCExpressionsMenu original, out ImmutableArray clonedMenus) + { return ClonedMappings.TryGetValue(original, out clonedMenus); } - public static VRCExpressionsMenu GetOriginal(VRCExpressionsMenu cloned) { + public static VRCExpressionsMenu GetOriginal(VRCExpressionsMenu cloned) + { return OriginalMapping.TryGetValue(cloned, out VRCExpressionsMenu original) ? original : null; } } diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuTreeView.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuTreeView.cs index 2cfdeaa2..004f908d 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuTreeView.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/AvMenuTreeView.cs @@ -122,20 +122,24 @@ namespace nadena.dev.modular_avatar.core.editor OnDoubleclickSelect.Invoke(); } - protected override TreeViewItem BuildRoot() { + protected override TreeViewItem BuildRoot() + { this._menuItems.Clear(); this._visitedMenuStack.Clear(); _menuTree = new MenuTree(Avatar); _menuTree.AvatarsMenuMapping(); - foreach (ModularAvatarMenuInstaller installer in this.Avatar.gameObject.GetComponentsInChildren(true)) { + foreach (ModularAvatarMenuInstaller installer in this.Avatar.gameObject.GetComponentsInChildren(true)) + { if (installer == Installer) continue; this._menuTree.MappingMenuInstaller(installer); } var root = new TreeViewItem(-1, -1, ""); - List treeItems = new List { - new TreeViewItem { + List treeItems = new List + { + new TreeViewItem + { id = 0, depth = 0, displayName = $"{Avatar.gameObject.name} ({(Avatar.expressionsMenu == null ? "None" : Avatar.expressionsMenu.name)})" @@ -149,15 +153,18 @@ namespace nadena.dev.modular_avatar.core.editor return root; } - private void TraverseMenu(int depth, List items, VRCExpressionsMenu menu) { + private void TraverseMenu(int depth, List items, VRCExpressionsMenu menu) + { IEnumerable children = this._menuTree.GetChildren(menu) .Where(child => !this._visitedMenuStack.Contains(child.menu)); - foreach (MenuTree.ChildElement child in children) { + foreach (MenuTree.ChildElement child in children) + { string displayName = child.installer == null ? $"{child.menuName} ({child.menu.name})" : $"{child.menuName} ({child.menu.name}) InstallerObject : {child.installer.name}"; items.Add( - new TreeViewItem { + new TreeViewItem + { id = items.Count, depth = depth, displayName = displayName @@ -169,53 +176,5 @@ namespace nadena.dev.modular_avatar.core.editor this._visitedMenuStack.Pop(); } } - - /* - protected override TreeViewItem BuildRoot() - { - _menuItems.Clear(); - _visitedMenus.Clear(); - - if (Avatar.expressionsMenu == null) - { - return new TreeViewItem(0, -1, "No menu"); - } - - _visitedMenus.Add(Avatar.expressionsMenu); - _menuItems.Add(Avatar.expressionsMenu); - var root = new TreeViewItem {id = -1, depth = -1, displayName = ""}; - - var treeItems = new List(); - treeItems.Add(new TreeViewItem - {id = 0, depth = 0, displayName = $"{Avatar.gameObject.name} ({Avatar.expressionsMenu.name})"}); - - TraverseMenu(1, treeItems, Avatar.expressionsMenu); - - SetupParentsAndChildrenFromDepths(root, treeItems); - - return root; - } - - private void TraverseMenu(int depth, List items, VRCExpressionsMenu menu) - { - foreach (var control in menu.controls) - { - if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu - && control.subMenu != null && !_visitedMenus.Contains(control.subMenu)) - { - items.Add(new TreeViewItem - { - id = _menuItems.Count, - depth = depth, - displayName = $"{control.name} ({control.subMenu.name})" - }); - _menuItems.Add(control.subMenu); - _visitedMenus.Add(control.subMenu); - - TraverseMenu(depth + 1, items, control.subMenu); - } - } - } - */ } } \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/MenuInstallHook.cs b/Packages/nadena.dev.modular-avatar/Editor/MenuInstallHook.cs index ea050890..a23e8f4e 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/MenuInstallHook.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/MenuInstallHook.cs @@ -1,5 +1,4 @@ -#define NEW -using System; +using System; using System.Collections.Generic; using System.Linq; using UnityEditor; @@ -22,11 +21,11 @@ namespace nadena.dev.modular_avatar.core.editor private VRCExpressionsMenu _rootMenu; -#if NEW private MenuTree _menuTree; private Stack _visitedInstallerStack; - public void OnPreprocessAvatar(GameObject avatarRoot) { + public void OnPreprocessAvatar(GameObject avatarRoot) + { ModularAvatarMenuInstaller[] menuInstallers = avatarRoot.GetComponentsInChildren(true) .Where(menuInstaller => menuInstaller.enabled) .ToArray(); @@ -38,7 +37,8 @@ namespace nadena.dev.modular_avatar.core.editor VRCAvatarDescriptor avatar = avatarRoot.GetComponent(); - if (avatar.expressionsMenu == null) { + if (avatar.expressionsMenu == null) + { var menu = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(menu, Util.GenerateAssetPath()); avatar.expressionsMenu = menu; @@ -51,16 +51,18 @@ namespace nadena.dev.modular_avatar.core.editor avatar.expressionsMenu = CloneMenu(avatar.expressionsMenu); - foreach (ModularAvatarMenuInstaller installer in menuInstallers) { + foreach (ModularAvatarMenuInstaller installer in menuInstallers) + { _menuTree.MappingMenuInstaller(installer); } - foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(null)) { - InstallMenuToAvatarMenu(childElement.installer); + foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(null)) + { + InstallMenu(childElement.installer); } } - private void InstallMenuToAvatarMenu(ModularAvatarMenuInstaller installer) + private void InstallMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu installTarget = null) { if (!installer.enabled) return; @@ -69,29 +71,9 @@ namespace nadena.dev.modular_avatar.core.editor installer.installTargetMenu = _rootMenu; } - if (installer.installTargetMenu == null || installer.menuToAppend == null) return; - if (!_clonedMenus.TryGetValue(installer.installTargetMenu, out var targetMenu)) return; - - // Clone before appending to sanitize menu icons - targetMenu.controls.AddRange(CloneMenu(installer.menuToAppend).controls); - - SplitMenu(installer, targetMenu); - - if (_visitedInstallerStack.Contains(installer)) return; - _visitedInstallerStack.Push(installer); - foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(installer)) { - InstallMenuToInstallerMenu(childElement.parent, childElement.installer); - } - - _visitedInstallerStack.Pop(); - } - - private void InstallMenuToInstallerMenu(VRCExpressionsMenu installTarget, ModularAvatarMenuInstaller installer) { - if (!installer.enabled) return; - - if (installer.installTargetMenu == null) + if (installTarget == null) { - installer.installTargetMenu = _rootMenu; + installTarget = installer.installTargetMenu; } if (installer.installTargetMenu == null || installer.menuToAppend == null) return; @@ -101,32 +83,37 @@ namespace nadena.dev.modular_avatar.core.editor targetMenu.controls.AddRange(CloneMenu(installer.menuToAppend).controls); SplitMenu(installer, targetMenu); - + if (_visitedInstallerStack.Contains(installer)) return; _visitedInstallerStack.Push(installer); - foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(installer)) { - InstallMenuToInstallerMenu(childElement.parent, childElement.installer); + foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(installer)) + { + InstallMenu(childElement.installer, childElement.parent); } _visitedInstallerStack.Pop(); } - private void SplitMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu targetMenu) { - while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS) { + private void SplitMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu targetMenu) + { + while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS) + { // Split target menu var newMenu = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath()); - var keepCount = VRCExpressionsMenu.MAX_CONTROLS - 1; + const int keepCount = VRCExpressionsMenu.MAX_CONTROLS - 1; newMenu.controls.AddRange(targetMenu.controls.Skip(keepCount)); targetMenu.controls.RemoveRange(keepCount, targetMenu.controls.Count - keepCount ); - targetMenu.controls.Add(new VRCExpressionsMenu.Control() { + targetMenu.controls.Add(new VRCExpressionsMenu.Control + { name = "More", type = VRCExpressionsMenu.Control.ControlType.SubMenu, subMenu = newMenu, - parameter = new VRCExpressionsMenu.Control.Parameter() { + parameter = new VRCExpressionsMenu.Control.Parameter + { name = "" }, subParameters = Array.Empty(), @@ -138,86 +125,7 @@ namespace nadena.dev.modular_avatar.core.editor targetMenu = newMenu; } } -#else - private Dictionary _installTargets; - - public void OnPreprocessAvatar(GameObject avatarRoot) - { - var menuInstallers = avatarRoot.GetComponentsInChildren(true) - .Where(c => c.enabled) - .ToArray(); - if (menuInstallers.Length == 0) return; - - _clonedMenus = new Dictionary(); - - var avatar = avatarRoot.GetComponent(); - - if (avatar.expressionsMenu == null) - { - var menu = ScriptableObject.CreateInstance(); - AssetDatabase.CreateAsset(menu, Util.GenerateAssetPath()); - avatar.expressionsMenu = menu; - } - - _rootMenu = avatar.expressionsMenu; - avatar.expressionsMenu = CloneMenu(avatar.expressionsMenu); - _installTargets = new Dictionary(_clonedMenus); - - foreach (var install in menuInstallers) - { - InstallMenu(install); - } - } - - private void InstallMenu(ModularAvatarMenuInstaller installer) - { - if (!installer.enabled) return; - - if (installer.installTargetMenu == null) - { - installer.installTargetMenu = _rootMenu; - } - - if (installer.installTargetMenu == null || installer.menuToAppend == null) return; - if (!_installTargets.TryGetValue(installer.installTargetMenu, out var targetMenu)) return; - if (_installTargets.ContainsKey(installer.menuToAppend)) return; - - // Clone before appending to sanitize menu icons - targetMenu.controls.AddRange(CloneMenu(installer.menuToAppend).controls); - - while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS) - { - // Split target menu - var newMenu = ScriptableObject.CreateInstance(); - AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath()); - var keepCount = VRCExpressionsMenu.MAX_CONTROLS - 1; - newMenu.controls.AddRange(targetMenu.controls.Skip(keepCount)); - targetMenu.controls.RemoveRange(keepCount, - targetMenu.controls.Count - keepCount - ); - - targetMenu.controls.Add(new VRCExpressionsMenu.Control() - { - name = "More", - type = VRCExpressionsMenu.Control.ControlType.SubMenu, - subMenu = newMenu, - parameter = new VRCExpressionsMenu.Control.Parameter() - { - name = "" - }, - subParameters = Array.Empty(), - icon = _moreIcon, - labels = Array.Empty() - }); - - _installTargets[installer.installTargetMenu] = newMenu; - targetMenu = newMenu; - } - } -#endif - - - + private VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu) { if (menu == null) return null; diff --git a/Packages/nadena.dev.modular-avatar/Editor/MenuTree.cs b/Packages/nadena.dev.modular-avatar/Editor/MenuTree.cs index 46029b9b..1f56c10e 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/MenuTree.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/MenuTree.cs @@ -6,10 +6,12 @@ using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.ScriptableObjects; using static VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu.Control; -// ReSharper disable once CheckNamespace -namespace nadena.dev.modular_avatar.core.editor { - public class MenuTree { - public struct ChildElement { +namespace nadena.dev.modular_avatar.core.editor +{ + public class MenuTree + { + public struct ChildElement + { public string menuName; public VRCExpressionsMenu menu; public VRCExpressionsMenu parent; @@ -17,61 +19,83 @@ namespace nadena.dev.modular_avatar.core.editor { public bool isInstallerRoot; } + private class ImmutableBuilder + { + public ImmutableArray immutableArray; + public ImmutableArray.Builder builder; + } + private readonly HashSet _included; private readonly VRCExpressionsMenu _rootMenu; - private readonly Dictionary> _menuChildrenMap; + private readonly Dictionary _menuChildrenMap; - public MenuTree(VRCAvatarDescriptor descriptor) { + public MenuTree(VRCAvatarDescriptor descriptor) + { this._rootMenu = descriptor.expressionsMenu; this._included = new HashSet(); - this._menuChildrenMap = new Dictionary>(); + this._menuChildrenMap = new Dictionary(); - if (this._rootMenu == null) { + if (this._rootMenu == null) + { this._rootMenu = ScriptableObject.CreateInstance(); } this._included.Add(this._rootMenu); } - public void AvatarsMenuMapping() { + public void AvatarsMenuMapping() + { if (this._rootMenu == null) return; this.MappingMenu(this._rootMenu); } - public void MappingMenuInstaller(ModularAvatarMenuInstaller installer) { + public void MappingMenuInstaller(ModularAvatarMenuInstaller installer) + { if (!installer.enabled) return; if (installer.menuToAppend == null) return; this.MappingMenu(installer); } - public IEnumerable GetChildren(VRCExpressionsMenu parent) { - // TODO: ライブラリとするのであれば、複製したリスト or ImmutableArray,を返すのが好ましい + public ImmutableArray GetChildren(VRCExpressionsMenu parent) + { if (parent == null) parent = this._rootMenu; - return this._menuChildrenMap.TryGetValue(parent, out List children) - ? children - : Enumerable.Empty(); + if (!this._menuChildrenMap.TryGetValue(parent, out ImmutableBuilder immutableBuilder)) return ImmutableArray.Empty; + if (immutableBuilder.immutableArray == ImmutableArray.Empty) + { + immutableBuilder.immutableArray = immutableBuilder.builder.ToImmutable(); + } + return immutableBuilder.immutableArray; } - public IEnumerable GetChildInstallers(ModularAvatarMenuInstaller parentInstaller) { + public IEnumerable GetChildInstallers(ModularAvatarMenuInstaller parentInstaller) + { HashSet visitedMenus = new HashSet(); Queue queue = new Queue(); if (parentInstaller != null && parentInstaller.menuToAppend == null) yield break; - if (parentInstaller == null) { + if (parentInstaller == null) + { queue.Enqueue(this._rootMenu); - } else { + } + else + { if (parentInstaller.menuToAppend == null) yield break; - foreach (KeyValuePair childMenu in GetChildMenus(parentInstaller.menuToAppend)) { + foreach (KeyValuePair childMenu in GetChildMenus(parentInstaller.menuToAppend)) + { queue.Enqueue(childMenu.Value); } } - while (queue.Count > 0) { + + while (queue.Count > 0) + { VRCExpressionsMenu parentMenu = queue.Dequeue(); if (visitedMenus.Contains(parentMenu)) continue; visitedMenus.Add(parentMenu); HashSet returnedInstallers = new HashSet(); - foreach (ChildElement childElement in this.GetChildren(parentMenu)) { - if (!childElement.isInstallerRoot) { + foreach (ChildElement childElement in this.GetChildren(parentMenu)) + { + if (!childElement.isInstallerRoot) + { queue.Enqueue(childElement.menu); continue; } @@ -84,51 +108,82 @@ namespace nadena.dev.modular_avatar.core.editor { } - private void MappingMenu(VRCExpressionsMenu root) { - foreach (KeyValuePair childMenu in GetChildMenus(root)) { - this.MappingMenu(root, new ChildElement { + private void MappingMenu(VRCExpressionsMenu root) + { + foreach (KeyValuePair childMenu in GetChildMenus(root)) + { + this.MappingMenu(root, new ChildElement + { menuName = childMenu.Key, menu = childMenu.Value }); } } - private void MappingMenu(ModularAvatarMenuInstaller installer) { + private void MappingMenu(ModularAvatarMenuInstaller installer) + { IEnumerable> childMenus = GetChildMenus(installer.menuToAppend); IEnumerable parents = Enumerable.Empty(); if (installer.installTargetMenu != null && - ClonedMenuMappings.TryGetClonedMenus(installer.installTargetMenu, out ImmutableArray parentMenus)) { + ClonedMenuMappings.TryGetClonedMenus(installer.installTargetMenu, out ImmutableArray parentMenus)) + { parents = parentMenus; } VRCExpressionsMenu[] parentsMenus = parents.DefaultIfEmpty(installer.installTargetMenu).ToArray(); - - foreach (KeyValuePair childMenu in childMenus) { - ChildElement childElement = new ChildElement { + bool isControlOnlyMenu = true; + foreach (KeyValuePair childMenu in childMenus) + { + isControlOnlyMenu = false; + ChildElement childElement = new ChildElement + { menuName = childMenu.Key, menu = childMenu.Value, installer = installer, isInstallerRoot = true }; - foreach (VRCExpressionsMenu parentMenu in parentsMenus) { + foreach (VRCExpressionsMenu parentMenu in parentsMenus) + { this.MappingMenu(parentMenu, childElement); } } + + if (!isControlOnlyMenu) return; + { + foreach (VRCExpressionsMenu parentMenu in parentsMenus) + { + this.MappingMenu(parentMenu, new ChildElement + { + installer = installer, + isInstallerRoot = true + }); + } + } } - private void MappingMenu(VRCExpressionsMenu parent, ChildElement childElement) { + private void MappingMenu(VRCExpressionsMenu parent, ChildElement childElement) + { if (parent == null) parent = this._rootMenu; childElement.parent = parent; - if (!this._menuChildrenMap.TryGetValue(parent, out List children)) { - children = new List(); + if (!this._menuChildrenMap.TryGetValue(parent, out ImmutableBuilder children)) + { + children = new ImmutableBuilder + { + builder = ImmutableArray.CreateBuilder(), + immutableArray = ImmutableArray.Empty + }; this._menuChildrenMap[parent] = children; } - children.Add(childElement); + children.builder.Add(childElement); + children.immutableArray = ImmutableArray.Empty; + if (childElement.menu == null) return; if (this._included.Contains(childElement.menu)) return; this._included.Add(childElement.menu); - foreach (KeyValuePair childMenu in GetChildMenus(childElement.menu)) { - this.MappingMenu(childElement.menu, new ChildElement { + foreach (KeyValuePair childMenu in GetChildMenus(childElement.menu)) + { + this.MappingMenu(childElement.menu, new ChildElement + { menuName = childMenu.Key, menu = childMenu.Value, installer = childElement.installer @@ -136,7 +191,8 @@ namespace nadena.dev.modular_avatar.core.editor { } } - private static IEnumerable> GetChildMenus(VRCExpressionsMenu expressionsMenu) { + private static IEnumerable> GetChildMenus(VRCExpressionsMenu expressionsMenu) + { return expressionsMenu.controls .Where(control => control.type == ControlType.SubMenu && control.subMenu != null) .Select(control => new KeyValuePair(control.name, control.subMenu));