integrate with menu generation pass and menu target UI

This commit is contained in:
bd_ 2023-02-22 21:54:09 +09:00
parent 19e42f3422
commit 52dd314f8a
8 changed files with 102 additions and 306 deletions

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using nadena.dev.modular_avatar.core.editor.menu;
using NUnit.Framework; using NUnit.Framework;
using UnityEditor; using UnityEditor;
using UnityEditor.IMGUI.Controls; using UnityEditor.IMGUI.Controls;
@ -28,7 +29,7 @@ namespace nadena.dev.modular_avatar.core.editor
set => _treeView.TargetInstaller = value; set => _treeView.TargetInstaller = value;
} }
public Action<VRCExpressionsMenu> OnMenuSelected = (menu) => { }; public Action<object> OnMenuSelected = (menu) => { };
private void Awake() private void Awake()
{ {
@ -58,7 +59,8 @@ namespace nadena.dev.modular_avatar.core.editor
_treeView.OnGUI(new Rect(0, 0, position.width, position.height)); _treeView.OnGUI(new Rect(0, 0, position.width, position.height));
} }
internal static void Show(VRCAvatarDescriptor Avatar, ModularAvatarMenuInstaller Installer, Action<VRCExpressionsMenu> OnSelect) internal static void Show(VRCAvatarDescriptor Avatar, ModularAvatarMenuInstaller Installer,
Action<object> OnSelect)
{ {
var window = GetWindow<AvMenuTreeViewWindow>(); var window = GetWindow<AvMenuTreeViewWindow>();
window.titleContent = new GUIContent("Select menu"); window.titleContent = new GUIContent("Select menu");
@ -97,14 +99,14 @@ namespace nadena.dev.modular_avatar.core.editor
} }
} }
internal Action<VRCExpressionsMenu> OnSelect = (menu) => { }; internal Action<object> OnSelect = (menu) => { };
internal Action OnDoubleclickSelect = () => { }; internal Action OnDoubleclickSelect = () => { };
private List<VRCExpressionsMenu> _menuItems = new List<VRCExpressionsMenu>(); private List<object> _nodeKeys = new List<object>();
private HashSet<VRCExpressionsMenu> _visitedMenus = new HashSet<VRCExpressionsMenu>(); private HashSet<object> _visitedMenus = new HashSet<object>();
private MenuTree _menuTree; private VirtualMenu _menuTree;
private Stack<VRCExpressionsMenu> _visitedMenuStack = new Stack<VRCExpressionsMenu>(); private Stack<object> _visitedMenuStack = new Stack<object>();
public AvMenuTreeView(TreeViewState state) : base(state) public AvMenuTreeView(TreeViewState state) : base(state)
{ {
@ -112,27 +114,21 @@ namespace nadena.dev.modular_avatar.core.editor
protected override void SelectionChanged(IList<int> selectedIds) protected override void SelectionChanged(IList<int> selectedIds)
{ {
OnSelect.Invoke(_menuItems[selectedIds[0]]); OnSelect.Invoke(_nodeKeys[selectedIds[0]]);
} }
protected override void DoubleClickedItem(int id) protected override void DoubleClickedItem(int id)
{ {
OnSelect.Invoke(_menuItems[id]); OnSelect.Invoke(_nodeKeys[id]);
OnDoubleclickSelect.Invoke(); OnDoubleclickSelect.Invoke();
} }
protected override TreeViewItem BuildRoot() protected override TreeViewItem BuildRoot()
{ {
_menuItems.Clear(); _nodeKeys.Clear();
_visitedMenuStack.Clear(); _visitedMenuStack.Clear();
_menuTree = new MenuTree(Avatar); _menuTree = VirtualMenu.ForAvatar(_avatar);
_menuTree.TraverseAvatarMenu();
foreach (ModularAvatarMenuInstaller installer in Avatar.gameObject.GetComponentsInChildren<ModularAvatarMenuInstaller>(true))
{
if (installer == TargetInstaller) continue;
_menuTree.TraverseMenuInstaller(installer);
}
var root = new TreeViewItem(-1, -1, "<root>"); var root = new TreeViewItem(-1, -1, "<root>");
List<TreeViewItem> treeItems = new List<TreeViewItem> List<TreeViewItem> treeItems = new List<TreeViewItem>
@ -141,27 +137,27 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
id = 0, id = 0,
depth = 0, depth = 0,
displayName = $"{Avatar.gameObject.name} ({(Avatar.expressionsMenu == null ? "None" : Avatar.expressionsMenu.name)})" displayName =
$"{Avatar.gameObject.name} ({(Avatar.expressionsMenu == null ? "None" : Avatar.expressionsMenu.name)})"
} }
}; };
_menuItems.Add(Avatar.expressionsMenu); _nodeKeys.Add(_menuTree.RootMenuKey);
_visitedMenuStack.Push(Avatar.expressionsMenu); _visitedMenuStack.Push(_menuTree.RootMenuKey);
TraverseMenu(1, treeItems, _menuTree.RootMenuNode);
TraverseMenu(1, treeItems, Avatar.expressionsMenu);
SetupParentsAndChildrenFromDepths(root, treeItems); SetupParentsAndChildrenFromDepths(root, treeItems);
return root; return root;
} }
private void TraverseMenu(int depth, List<TreeViewItem> items, VRCExpressionsMenu menu) private void TraverseMenu(int depth, List<TreeViewItem> items, MenuNode node)
{ {
IEnumerable<MenuTree.ChildElement> children = _menuTree.GetChildren(menu) IEnumerable<VirtualControl> children = node.Controls
.Where(child => !_visitedMenuStack.Contains(child.menu)); .Where(control => control.type == VRCExpressionsMenu.Control.ControlType.SubMenu &&
foreach (MenuTree.ChildElement child in children) control.SubmenuNode != null &&
!_visitedMenuStack.Contains(control.SubmenuNode));
foreach (var child in children)
{ {
if (child.menu == null) continue; string displayName = child.name;
string displayName = child.installer == null ?
$"{child.menuName} ({child.menu.name})" :
$"{child.menuName} ({child.menu.name}) InstallerObject : {child.installer.name}";
items.Add( items.Add(
new TreeViewItem new TreeViewItem
{ {
@ -170,9 +166,9 @@ namespace nadena.dev.modular_avatar.core.editor
displayName = displayName displayName = displayName
} }
); );
_menuItems.Add(child.menu); _nodeKeys.Add(child.SubmenuNode.NodeKey);
_visitedMenuStack.Push(child.menu); _visitedMenuStack.Push(child.SubmenuNode);
TraverseMenu(depth + 1, items, child.menu); TraverseMenu(depth + 1, items, child.SubmenuNode);
_visitedMenuStack.Pop(); _visitedMenuStack.Pop();
} }
} }

View File

@ -26,8 +26,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (name != null) if (name != null)
{ {
EditorGUI.BeginChangeCheck(); EditorGUI.BeginChangeCheck();
var targetGameObject = ((ModularAvatarMenuItem) target).gameObject; var newName = EditorGUILayout.TextField("Name", name);
var newName = EditorGUILayout.TextField("Name", targetGameObject.name);
if (EditorGUI.EndChangeCheck() && commitName != null) if (EditorGUI.EndChangeCheck() && commitName != null)
{ {
commitName(newName); commitName(newName);

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using nadena.dev.modular_avatar.core.editor.menu;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.Components;
@ -101,7 +102,20 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
AvMenuTreeViewWindow.Show(avatar, _installer, menu => AvMenuTreeViewWindow.Show(avatar, _installer, menu =>
{ {
installTo.objectReferenceValue = menu; if (menu is VRCExpressionsMenu expMenu)
{
if (expMenu == avatar.expressionsMenu) installTo.objectReferenceValue = null;
else installTo.objectReferenceValue = expMenu;
}
else if (menu is RootMenu)
{
installTo.objectReferenceValue = null;
}
else if (menu is ModularAvatarMenuItem item)
{
// TODO
}
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
}); });
} }

View File

@ -4,6 +4,7 @@ using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting; using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects; using VRC.SDK3.Avatars.ScriptableObjects;
namespace nadena.dev.modular_avatar.core.editor.menu namespace nadena.dev.modular_avatar.core.editor.menu
@ -71,7 +72,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
*/ */
internal class VirtualMenu internal class VirtualMenu
{ {
private readonly object RootMenuKey; internal readonly object RootMenuKey;
/// <summary> /// <summary>
/// Indexes which menu installers are contributing to which VRCExpressionMenu assets. /// Indexes which menu installers are contributing to which VRCExpressionMenu assets.
@ -92,6 +93,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
// TODO: immutable? // TODO: immutable?
public Dictionary<object, MenuNode> ResolvedMenu => _resolvedMenu; public Dictionary<object, MenuNode> ResolvedMenu => _resolvedMenu;
public MenuNode RootMenuNode => ResolvedMenu[RootMenuKey];
/// <summary> /// <summary>
/// Initializes the VirtualMenu. /// Initializes the VirtualMenu.
@ -111,6 +113,24 @@ namespace nadena.dev.modular_avatar.core.editor.menu
} }
} }
internal static VirtualMenu ForAvatar(VRCAvatarDescriptor avatar)
{
var menu = new VirtualMenu(avatar.expressionsMenu);
foreach (var installer in avatar.GetComponentsInChildren<ModularAvatarMenuInstaller>(true))
{
menu.RegisterMenuInstaller(installer);
}
foreach (var target in avatar.GetComponentsInChildren<ModularAvatarMenuInstallTarget>(true))
{
menu.RegisterMenuInstallTarget(target);
}
menu.FreezeMenu();
return menu;
}
private MenuNode ImportMenu(VRCExpressionsMenu menu, object menuKey = null) private MenuNode ImportMenu(VRCExpressionsMenu menu, object menuKey = null)
{ {
if (menuKey == null) menuKey = menu; if (menuKey == null) menuKey = menu;

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using nadena.dev.modular_avatar.core.editor.menu;
using nadena.dev.modular_avatar.editor.ErrorReporting; using nadena.dev.modular_avatar.editor.ErrorReporting;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
@ -22,7 +23,6 @@ namespace nadena.dev.modular_avatar.core.editor
private VRCExpressionsMenu _rootMenu; private VRCExpressionsMenu _rootMenu;
private MenuTree _menuTree;
private Stack<ModularAvatarMenuInstaller> _visitedInstallerStack; private Stack<ModularAvatarMenuInstaller> _visitedInstallerStack;
public void OnPreprocessAvatar(GameObject avatarRoot, BuildContext context) public void OnPreprocessAvatar(GameObject avatarRoot, BuildContext context)
@ -48,55 +48,15 @@ namespace nadena.dev.modular_avatar.core.editor
} }
_rootMenu = avatar.expressionsMenu; _rootMenu = avatar.expressionsMenu;
_menuTree = new MenuTree(avatar); var virtualMenu = VirtualMenu.ForAvatar(avatar);
_menuTree.TraverseAvatarMenu(); avatar.expressionsMenu = virtualMenu.SerializeMenu(asset =>
avatar.expressionsMenu = _context.CloneMenu(avatar.expressionsMenu);
foreach (ModularAvatarMenuInstaller installer in menuInstallers)
{ {
BuildReport.ReportingObject(installer, () => _menuTree.TraverseMenuInstaller(installer)); context.SaveAsset(asset);
} if (asset is VRCExpressionsMenu menu) SplitMenu(menu);
});
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(null))
{
BuildReport.ReportingObject(childElement.installer, () => InstallMenu(childElement.installer));
}
} }
private void InstallMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu installTarget = null) private void SplitMenu(VRCExpressionsMenu targetMenu)
{
if (!installer.enabled) return;
if (installer.installTargetMenu == null)
{
installer.installTargetMenu = _rootMenu;
}
if (installTarget == null)
{
installTarget = installer.installTargetMenu;
}
if (installer.installTargetMenu == null || installer.menuToAppend == null) return;
if (!_context.ClonedMenus.TryGetValue(installTarget, out var targetMenu)) return;
// Clone before appending to sanitize menu icons
targetMenu.controls.AddRange(_context.CloneMenu(installer.menuToAppend).controls);
SplitMenu(installer, targetMenu);
if (_visitedInstallerStack.Contains(installer)) return;
_visitedInstallerStack.Push(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) while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS)
{ {
@ -123,7 +83,6 @@ namespace nadena.dev.modular_avatar.core.editor
labels = Array.Empty<VRCExpressionsMenu.Control.Label>() labels = Array.Empty<VRCExpressionsMenu.Control.Label>()
}); });
_context.ClonedMenus[installer.installTargetMenu] = newMenu;
targetMenu = newMenu; targetMenu = newMenu;
} }
} }

View File

@ -1,209 +0,0 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
using static VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu.Control;
namespace nadena.dev.modular_avatar.core.editor
{
internal class MenuTree
{
public struct ChildElement
{
/// <summary>
/// Parent menu control name
/// </summary>
public string menuName;
public VRCExpressionsMenu menu;
public VRCExpressionsMenu parent;
/// <summary>
/// Installer to install this menu. Is null if the this menu is not installed by the installer.
/// </summary>
public ModularAvatarMenuInstaller installer;
/// <summary>
/// Whether the this submenu is added directly by the installer
/// </summary>
public bool isInstallerRoot;
}
private readonly HashSet<VRCExpressionsMenu> _included;
private readonly VRCExpressionsMenu _rootMenu;
/// <summary>
/// Map to link child menus from parent menu
/// </summary>
private readonly Dictionary<VRCExpressionsMenu, ImmutableList<ChildElement>> _menuChildrenMap;
public MenuTree(VRCAvatarDescriptor descriptor)
{
_rootMenu = descriptor.expressionsMenu;
_included = new HashSet<VRCExpressionsMenu>();
_menuChildrenMap = new Dictionary<VRCExpressionsMenu, ImmutableList<ChildElement>>();
if (_rootMenu == null)
{
// If the route menu is null, create a temporary menu indicating the route
_rootMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
}
_included.Add(_rootMenu);
}
public void TraverseAvatarMenu()
{
if (_rootMenu == null) return;
TraverseMenu(_rootMenu);
}
public void TraverseMenuInstaller(ModularAvatarMenuInstaller installer)
{
if (!installer.enabled) return;
if (installer.menuToAppend == null) return;
TraverseMenu(installer);
}
public ImmutableList<ChildElement> GetChildren(VRCExpressionsMenu parent)
{
if (parent == null) parent = _rootMenu;
return !_menuChildrenMap.TryGetValue(parent, out ImmutableList<ChildElement> immutableList) ? ImmutableList<ChildElement>.Empty : immutableList;
}
public IEnumerable<ChildElement> GetChildInstallers(ModularAvatarMenuInstaller parentInstaller)
{
HashSet<VRCExpressionsMenu> visitedMenus = new HashSet<VRCExpressionsMenu>();
Queue<VRCExpressionsMenu> queue = new Queue<VRCExpressionsMenu>();
if (parentInstaller != null && parentInstaller.menuToAppend == null) yield break;
if (parentInstaller == null)
{
queue.Enqueue(_rootMenu);
}
else
{
if (parentInstaller.menuToAppend == null) yield break;
foreach (KeyValuePair<string, VRCExpressionsMenu> childMenu in GetChildMenus(parentInstaller.menuToAppend))
{
queue.Enqueue(childMenu.Value);
}
}
while (queue.Count > 0)
{
VRCExpressionsMenu parentMenu = queue.Dequeue();
if (visitedMenus.Contains(parentMenu)) continue;
visitedMenus.Add(parentMenu);
HashSet<ModularAvatarMenuInstaller> returnedInstallers = new HashSet<ModularAvatarMenuInstaller>();
foreach (ChildElement childElement in GetChildren(parentMenu))
{
if (!childElement.isInstallerRoot)
{
queue.Enqueue(childElement.menu);
continue;
}
// One installer may add multiple children, so filter to return only one.
if (returnedInstallers.Contains(childElement.installer)) continue;
returnedInstallers.Add(childElement.installer);
yield return childElement;
}
}
}
private void TraverseMenu(VRCExpressionsMenu root)
{
foreach (KeyValuePair<string, VRCExpressionsMenu> childMenu in GetChildMenus(root))
{
TraverseMenu(root, new ChildElement
{
menuName = childMenu.Key,
menu = childMenu.Value
});
}
}
private void TraverseMenu(ModularAvatarMenuInstaller installer)
{
IEnumerable<KeyValuePair<string, VRCExpressionsMenu>> childMenus = GetChildMenus(installer.menuToAppend);
IEnumerable<VRCExpressionsMenu> parents = Enumerable.Empty<VRCExpressionsMenu>();
if (installer.installTargetMenu != null &&
ClonedMenuMappings.TryGetClonedMenus(installer.installTargetMenu, out ImmutableList<VRCExpressionsMenu> parentMenus))
{
parents = parentMenus;
}
VRCExpressionsMenu[] parentsMenus = parents.DefaultIfEmpty(installer.installTargetMenu).ToArray();
bool hasChildMenu = false;
/*
* Installer adds the controls in specified menu to the installation destination.
* So, since the specified menu itself does not exist as a child menu,
* and the child menus of the specified menu are the actual child menus, a single installer may add multiple child menus.
*/
foreach (KeyValuePair<string, VRCExpressionsMenu> childMenu in childMenus)
{
hasChildMenu = true;
ChildElement childElement = new ChildElement
{
menuName = childMenu.Key,
menu = childMenu.Value,
installer = installer,
isInstallerRoot = true
};
foreach (VRCExpressionsMenu parentMenu in parentsMenus)
{
TraverseMenu(parentMenu, childElement);
}
}
if (hasChildMenu) return;
/*
* If the specified menu does not have any submenus, it is not mapped as a child menu and the Installer information itself is not registered.
* Therefore, register elements that do not have child menus themselves, but only have information about the installer.
*/
foreach (VRCExpressionsMenu parentMenu in parentsMenus)
{
TraverseMenu(parentMenu, new ChildElement
{
installer = installer,
isInstallerRoot = true
});
}
}
private void TraverseMenu(VRCExpressionsMenu parent, ChildElement childElement)
{
if (parent == null) parent = _rootMenu;
childElement.parent = parent;
if (!_menuChildrenMap.TryGetValue(parent, out ImmutableList<ChildElement> children))
{
children = ImmutableList<ChildElement>.Empty;
_menuChildrenMap[parent] = children;
}
_menuChildrenMap[parent] = children.Add(childElement);
if (childElement.menu == null) return;
if (_included.Contains(childElement.menu)) return;
_included.Add(childElement.menu);
foreach (KeyValuePair<string, VRCExpressionsMenu> childMenu in GetChildMenus(childElement.menu))
{
TraverseMenu(childElement.menu, new ChildElement
{
menuName = childMenu.Key,
menu = childMenu.Value,
installer = childElement.installer
});
}
}
private static IEnumerable<KeyValuePair<string, VRCExpressionsMenu>> GetChildMenus(VRCExpressionsMenu expressionsMenu)
{
return expressionsMenu.controls
.Where(control => control.type == ControlType.SubMenu && control.subMenu != null)
.Select(control => new KeyValuePair<string, VRCExpressionsMenu>(control.name, control.subMenu));
}
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: effd4557902f4578af42d3bdfb7f876d
timeCreated: 1670746991

View File

@ -178,6 +178,26 @@ namespace nadena.dev.modular_avatar.core.editor
break; break;
} }
case ModularAvatarMenuItem menuItem:
{
if (menuItem.Control.parameter?.name != null &&
remaps.TryGetValue(menuItem.Control.parameter.name, out var newVal))
{
menuItem.Control.parameter.name = newVal;
}
foreach (var subParam in menuItem.Control.subParameters ??
Array.Empty<VRCExpressionsMenu.Control.Parameter>())
{
if (subParam?.name != null && remaps.TryGetValue(subParam.name, out var subNewVal))
{
subParam.name = subNewVal;
}
}
break;
}
} }
}); });
} }