mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-29 02:35:06 +08:00
feat: add object-based menu system (#218)
This adds a GameObject/Component based menu editing system, heavily inspired by Hai's Expressions Menu Hierarchy editor (https://github.com/hai-vr/av3-expressions-menu-hierarchy-editor)
This commit is contained in:
parent
ce76bb2190
commit
d39e17a8f6
@ -15,13 +15,13 @@ namespace modular_avatar_tests
|
||||
private const string MinimalAvatarGuid = "60d3416d1f6af4a47bf9056aefc38333";
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
public virtual void Setup()
|
||||
{
|
||||
objects = new List<GameObject>();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void Teardown()
|
||||
public virtual void Teardown()
|
||||
{
|
||||
foreach (var obj in objects)
|
||||
{
|
||||
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10cb5a7057c1452d8e1caf299c31b05b
|
||||
timeCreated: 1676981463
|
@ -0,0 +1,609 @@
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.modular_avatar.core;
|
||||
using nadena.dev.modular_avatar.core.editor.menu;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEditor.VersionControl;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace modular_avatar_tests.VirtualMenuTests
|
||||
{
|
||||
public class VirtualMenuTests : TestBase
|
||||
{
|
||||
private Texture2D testTex;
|
||||
private List<UnityEngine.Object> toDestroy;
|
||||
private int controlIndex;
|
||||
|
||||
public override void Setup()
|
||||
{
|
||||
base.Setup();
|
||||
testTex = new Texture2D(1, 1);
|
||||
toDestroy = new List<UnityEngine.Object>();
|
||||
controlIndex = 0;
|
||||
}
|
||||
|
||||
public override void Teardown()
|
||||
{
|
||||
base.Teardown();
|
||||
Object.DestroyImmediate(testTex);
|
||||
foreach (var obj in toDestroy)
|
||||
{
|
||||
Object.DestroyImmediate(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private T Create<T>(string name = null) where T : ScriptableObject
|
||||
{
|
||||
if (name == null) name = GUID.Generate().ToString();
|
||||
|
||||
T val = ScriptableObject.CreateInstance<T>();
|
||||
val.name = name;
|
||||
|
||||
toDestroy.Add(val);
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmptyMenu()
|
||||
{
|
||||
var virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
|
||||
var root = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(0, root.Controls.Count);
|
||||
Assert.AreSame(RootMenu.Instance, root.NodeKey);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicMenu()
|
||||
{
|
||||
var rootMenu = Create<VRCExpressionsMenu>();
|
||||
|
||||
rootMenu.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl(),
|
||||
GenerateTestControl()
|
||||
};
|
||||
|
||||
var virtualMenu = new VirtualMenu(rootMenu);
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
|
||||
var root = virtualMenu.ResolvedMenu[rootMenu];
|
||||
Assert.AreEqual(2, root.Controls.Count);
|
||||
Assert.AreSame(rootMenu, root.NodeKey);
|
||||
AssertControlEquals(rootMenu.controls[0], root.Controls[0]);
|
||||
AssertControlEquals(rootMenu.controls[1], root.Controls[1]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNativeMenuWithCycles()
|
||||
{
|
||||
var rootMenu = Create<VRCExpressionsMenu>("root");
|
||||
var sub1 = Create<VRCExpressionsMenu>("sub1");
|
||||
var sub2 = Create<VRCExpressionsMenu>("sub2");
|
||||
|
||||
rootMenu.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestSubmenu(sub1)
|
||||
};
|
||||
|
||||
sub1.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestSubmenu(sub2)
|
||||
};
|
||||
|
||||
sub2.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestSubmenu(rootMenu)
|
||||
};
|
||||
|
||||
var virtualMenu = new VirtualMenu(rootMenu);
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
Assert.AreEqual(3, virtualMenu.ResolvedMenu.Count);
|
||||
var rootNode = virtualMenu.ResolvedMenu[rootMenu];
|
||||
var sub1Node = virtualMenu.ResolvedMenu[sub1];
|
||||
var sub2Node = virtualMenu.ResolvedMenu[sub2];
|
||||
|
||||
Assert.AreEqual(1, rootNode.Controls.Count);
|
||||
Assert.AreSame(rootMenu, rootNode.NodeKey);
|
||||
Assert.AreSame(sub1Node, rootNode.Controls[0].SubmenuNode);
|
||||
Assert.IsNull(rootNode.Controls[0].subMenu);
|
||||
|
||||
Assert.AreEqual(1, sub1Node.Controls.Count);
|
||||
Assert.AreSame(sub1, sub1Node.NodeKey);
|
||||
Assert.AreSame(sub2Node, sub1Node.Controls[0].SubmenuNode);
|
||||
Assert.IsNull(sub1Node.Controls[0].subMenu);
|
||||
|
||||
Assert.AreEqual(1, sub2Node.Controls.Count);
|
||||
Assert.AreSame(sub2, sub2Node.NodeKey);
|
||||
Assert.AreSame(rootNode, sub2Node.Controls[0].SubmenuNode);
|
||||
Assert.IsNull(sub2Node.Controls[0].subMenu);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicMenuInstaller()
|
||||
{
|
||||
VRCExpressionsMenu testMenu = Create<VRCExpressionsMenu>();
|
||||
testMenu.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl(),
|
||||
GenerateTestControl()
|
||||
};
|
||||
|
||||
var installer = CreateInstaller("test");
|
||||
installer.menuToAppend = testMenu;
|
||||
|
||||
var virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer);
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
|
||||
var root = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(2, root.Controls.Count);
|
||||
Assert.AreSame(RootMenu.Instance, root.NodeKey);
|
||||
AssertControlEquals(testMenu.controls[0], root.Controls[0]);
|
||||
AssertControlEquals(testMenu.controls[1], root.Controls[1]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMenuItemInstaller()
|
||||
{
|
||||
var installer = CreateInstaller("test");
|
||||
installer.menuToAppend = Create<VRCExpressionsMenu>();
|
||||
|
||||
var item = installer.gameObject.AddComponent<ModularAvatarMenuItem>();
|
||||
item.Control = GenerateTestControl();
|
||||
|
||||
var virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer);
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
item.Control.name = "test";
|
||||
|
||||
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
|
||||
var root = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(1, root.Controls.Count);
|
||||
Assert.AreSame(RootMenu.Instance, root.NodeKey);
|
||||
AssertControlEquals(item.Control, root.Controls[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInstallOntoInstaller()
|
||||
{
|
||||
var installer_a = CreateInstaller("a");
|
||||
var installer_b = CreateInstaller("b");
|
||||
var installer_c = CreateInstaller("c");
|
||||
|
||||
var menu_a = Create<VRCExpressionsMenu>();
|
||||
var menu_b = Create<VRCExpressionsMenu>();
|
||||
var menu_c = Create<VRCExpressionsMenu>();
|
||||
|
||||
menu_a.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
menu_b.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
menu_c.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
|
||||
installer_a.menuToAppend = menu_a;
|
||||
installer_b.menuToAppend = menu_b;
|
||||
installer_c.menuToAppend = menu_c;
|
||||
|
||||
installer_c.installTargetMenu = installer_a.menuToAppend;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
virtualMenu.RegisterMenuInstaller(installer_b);
|
||||
virtualMenu.RegisterMenuInstaller(installer_c);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
|
||||
var rootMenu = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(3, rootMenu.Controls.Count);
|
||||
Assert.AreSame(RootMenu.Instance, rootMenu.NodeKey);
|
||||
AssertControlEquals(menu_a.controls[0], rootMenu.Controls[0]);
|
||||
AssertControlEquals(menu_c.controls[0], rootMenu.Controls[1]);
|
||||
AssertControlEquals(menu_b.controls[0], rootMenu.Controls[2]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInstallSubmenu()
|
||||
{
|
||||
var installer_a = CreateInstaller("a");
|
||||
|
||||
var menu_a = Create<VRCExpressionsMenu>("a");
|
||||
var menu_b = Create<VRCExpressionsMenu>("b");
|
||||
menu_a.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestSubmenu(menu_b)
|
||||
};
|
||||
menu_b.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
|
||||
installer_a.menuToAppend = menu_a;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
Assert.AreEqual(2, virtualMenu.ResolvedMenu.Count);
|
||||
var rootMenu = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
var subMenu = virtualMenu.ResolvedMenu[menu_b];
|
||||
Assert.AreSame(subMenu, rootMenu.Controls[0].SubmenuNode);
|
||||
Assert.AreSame(RootMenu.Instance, rootMenu.NodeKey);
|
||||
Assert.AreSame(menu_b, subMenu.NodeKey);
|
||||
Assert.AreEqual(1, subMenu.Controls.Count);
|
||||
AssertControlEquals(menu_b.controls[0], subMenu.Controls[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestYankInstaller()
|
||||
{
|
||||
var installer_a = CreateInstaller("a");
|
||||
var installer_b = CreateInstaller("b");
|
||||
|
||||
var menu_a = Create<VRCExpressionsMenu>("a");
|
||||
menu_a.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
installer_a.menuToAppend = menu_a;
|
||||
|
||||
var item = installer_b.gameObject.AddComponent<ModularAvatarMenuItem>();
|
||||
item.Control = GenerateTestControl();
|
||||
item.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
item.MenuSource = SubmenuSource.Children;
|
||||
|
||||
var child = CreateChild(item.gameObject, "child");
|
||||
var childItem = child.AddComponent<ModularAvatarMenuInstallTarget>();
|
||||
childItem.installer = installer_a;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
virtualMenu.RegisterMenuInstaller(installer_b);
|
||||
virtualMenu.RegisterMenuInstallTarget(childItem);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
item.Control.name = "b";
|
||||
|
||||
Assert.AreEqual(2, virtualMenu.ResolvedMenu.Count);
|
||||
var rootMenu = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
var item_node = virtualMenu.ResolvedMenu[new MenuNodesUnder(item.gameObject)];
|
||||
Assert.AreEqual(1, rootMenu.Controls.Count);
|
||||
Assert.AreSame(RootMenu.Instance, rootMenu.NodeKey);
|
||||
AssertControlEquals(item.Control, rootMenu.Controls[0]);
|
||||
Assert.AreSame(item_node, rootMenu.Controls[0].SubmenuNode);
|
||||
|
||||
Assert.AreEqual(1, item_node.Controls.Count);
|
||||
Assert.AreEqual(new MenuNodesUnder(item.gameObject), item_node.NodeKey);
|
||||
AssertControlEquals(menu_a.controls[0], item_node.Controls[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WhenMenuInstallersLoop_LoopIsTerminated()
|
||||
{
|
||||
var installer_a = CreateInstaller("a");
|
||||
var installer_b = CreateInstaller("b");
|
||||
|
||||
var menu_a = Create<VRCExpressionsMenu>("a");
|
||||
menu_a.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
installer_a.menuToAppend = menu_a;
|
||||
|
||||
var menu_b = Create<VRCExpressionsMenu>("b");
|
||||
menu_b.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
installer_b.menuToAppend = menu_b;
|
||||
|
||||
installer_a.installTargetMenu = menu_b;
|
||||
installer_b.installTargetMenu = menu_a;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(menu_a);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
virtualMenu.RegisterMenuInstaller(installer_b);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
Assert.AreEqual(1, virtualMenu.ResolvedMenu.Count);
|
||||
var rootMenu = virtualMenu.ResolvedMenu[menu_a];
|
||||
var menu_a_node = virtualMenu.ResolvedMenu[menu_a];
|
||||
Assert.AreEqual(3, rootMenu.Controls.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExternalSubmenuSource_WithMenuInstaller()
|
||||
{
|
||||
var installer_a = CreateInstaller("a");
|
||||
var installer_b = CreateInstaller("b");
|
||||
|
||||
var menu_a = Create<VRCExpressionsMenu>("a");
|
||||
menu_a.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
|
||||
var menu_b = Create<VRCExpressionsMenu>("b");
|
||||
menu_b.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
|
||||
var item_a = installer_a.gameObject.AddComponent<ModularAvatarMenuItem>();
|
||||
item_a.Control = GenerateTestControl();
|
||||
item_a.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
item_a.MenuSource = SubmenuSource.MenuAsset;
|
||||
item_a.Control.subMenu = menu_a;
|
||||
|
||||
installer_b.menuToAppend = menu_b;
|
||||
installer_b.installTargetMenu = menu_a;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
virtualMenu.RegisterMenuInstaller(installer_b);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
var rootNode = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(VRCExpressionsMenu.Control.ControlType.SubMenu, rootNode.Controls[0].type);
|
||||
var menu_a_node = rootNode.Controls[0].SubmenuNode;
|
||||
AssertControlEquals(menu_a.controls[0], menu_a_node.Controls[0]);
|
||||
AssertControlEquals(menu_b.controls[0], menu_a_node.Controls[1]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MenuItem_NestedSubmenuNodes()
|
||||
{
|
||||
var installer_a = CreateInstaller("root");
|
||||
var root_item = installer_a.gameObject.AddComponent<ModularAvatarMenuItem>();
|
||||
root_item.Control = GenerateTestControl();
|
||||
root_item.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
root_item.MenuSource = SubmenuSource.Children;
|
||||
|
||||
var mid_obj = CreateChild(root_item.gameObject, "mid");
|
||||
var mid_item = mid_obj.AddComponent<ModularAvatarMenuItem>();
|
||||
mid_item.Control = GenerateTestControl();
|
||||
mid_item.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
mid_item.MenuSource = SubmenuSource.Children;
|
||||
|
||||
var leaf_obj = CreateChild(mid_obj, "leaf");
|
||||
var leaf_item = leaf_obj.AddComponent<ModularAvatarMenuItem>();
|
||||
leaf_item.Control = GenerateTestControl();
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
var rootNode = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(VRCExpressionsMenu.Control.ControlType.SubMenu, rootNode.Controls[0].type);
|
||||
var mid_node = rootNode.Controls[0].SubmenuNode;
|
||||
var leaf_node = mid_node.Controls[0].SubmenuNode;
|
||||
|
||||
Assert.AreEqual(1, rootNode.Controls.Count);
|
||||
Assert.AreEqual(1, mid_node.Controls.Count);
|
||||
Assert.AreEqual(1, leaf_node.Controls.Count);
|
||||
|
||||
root_item.Control.name = "root";
|
||||
mid_item.Control.name = "mid";
|
||||
leaf_item.Control.name = "leaf";
|
||||
|
||||
AssertControlEquals(root_item.Control, rootNode.Controls[0]);
|
||||
AssertControlEquals(mid_item.Control, mid_node.Controls[0]);
|
||||
AssertControlEquals(leaf_item.Control, leaf_node.Controls[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MenuItem_RemoteReference()
|
||||
{
|
||||
var installer_a = CreateInstaller("root");
|
||||
var root_item = installer_a.gameObject.AddComponent<ModularAvatarMenuItem>();
|
||||
|
||||
var extern_root = CreateRoot("test");
|
||||
var extern_obj = CreateChild(extern_root, "control");
|
||||
var extern_item = extern_obj.AddComponent<ModularAvatarMenuItem>();
|
||||
extern_item.Control = GenerateTestControl();
|
||||
|
||||
root_item.Control = GenerateTestControl();
|
||||
root_item.Control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
root_item.MenuSource = SubmenuSource.Children;
|
||||
root_item.menuSource_otherObjectChildren = extern_root;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
root_item.Control.name = "root";
|
||||
extern_item.Control.name = "control";
|
||||
|
||||
var rootNode = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(VRCExpressionsMenu.Control.ControlType.SubMenu, rootNode.Controls[0].type);
|
||||
var extern_node = rootNode.Controls[0].SubmenuNode;
|
||||
|
||||
Assert.AreEqual(1, rootNode.Controls.Count);
|
||||
Assert.AreEqual(1, extern_node.Controls.Count);
|
||||
|
||||
AssertControlEquals(root_item.Control, rootNode.Controls[0]);
|
||||
AssertControlEquals(extern_item.Control, extern_node.Controls[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InstallerInstallsInstallTarget()
|
||||
{
|
||||
var installer_a = CreateInstaller("a");
|
||||
var installer_b = CreateInstaller("b");
|
||||
|
||||
var menu_b = Create<VRCExpressionsMenu>("b");
|
||||
menu_b.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl()
|
||||
};
|
||||
installer_b.menuToAppend = menu_b;
|
||||
|
||||
var item_a = installer_a.gameObject.AddComponent<ModularAvatarMenuInstallTarget>();
|
||||
item_a.installer = installer_b;
|
||||
|
||||
VirtualMenu virtualMenu = new VirtualMenu(null);
|
||||
virtualMenu.RegisterMenuInstaller(installer_a);
|
||||
virtualMenu.RegisterMenuInstaller(installer_b);
|
||||
virtualMenu.RegisterMenuInstallTarget(item_a);
|
||||
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
var rootNode = virtualMenu.ResolvedMenu[RootMenu.Instance];
|
||||
Assert.AreEqual(1, rootNode.Controls.Count);
|
||||
AssertControlEquals(menu_b.controls[0], rootNode.Controls[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSerializeMenu()
|
||||
{
|
||||
var menu_a = Create<VRCExpressionsMenu>("test");
|
||||
var menu_b = Create<VRCExpressionsMenu>("test2");
|
||||
var menu_c = Create<VRCExpressionsMenu>("test3");
|
||||
|
||||
menu_a.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl(),
|
||||
GenerateTestSubmenu(menu_b),
|
||||
};
|
||||
|
||||
menu_b.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestControl(),
|
||||
GenerateTestSubmenu(menu_c),
|
||||
};
|
||||
|
||||
menu_c.controls = new List<VRCExpressionsMenu.Control>()
|
||||
{
|
||||
GenerateTestSubmenu(menu_a),
|
||||
};
|
||||
|
||||
var virtualMenu = new VirtualMenu(menu_a);
|
||||
virtualMenu.FreezeMenu();
|
||||
|
||||
var assetSet = new HashSet<UnityEngine.Object>();
|
||||
var serialized = virtualMenu.SerializeMenu(obj => assetSet.Add(obj));
|
||||
|
||||
Assert.AreEqual(3, assetSet.Count);
|
||||
Assert.AreEqual(2, serialized.controls.Count);
|
||||
|
||||
AssertControlEquals(menu_a.controls[0], serialized.controls[0]);
|
||||
AssertControlEquals(menu_a.controls[1], serialized.controls[1]);
|
||||
|
||||
var serialized_b = serialized.controls[1].subMenu;
|
||||
Assert.AreEqual(2, serialized_b.controls.Count);
|
||||
|
||||
AssertControlEquals(menu_b.controls[0], serialized_b.controls[0]);
|
||||
AssertControlEquals(menu_b.controls[1], serialized_b.controls[1]);
|
||||
|
||||
var serialized_c = serialized_b.controls[1].subMenu;
|
||||
Assert.AreEqual(1, serialized_c.controls.Count);
|
||||
|
||||
AssertControlEquals(menu_c.controls[0], serialized_c.controls[0]);
|
||||
|
||||
Assert.True(assetSet.Contains(serialized));
|
||||
Assert.True(assetSet.Contains(serialized_b));
|
||||
Assert.True(assetSet.Contains(serialized_c));
|
||||
}
|
||||
|
||||
|
||||
ModularAvatarMenuInstaller CreateInstaller(string name)
|
||||
{
|
||||
GameObject obj = new GameObject();
|
||||
obj.name = name;
|
||||
|
||||
var installer = obj.AddComponent<ModularAvatarMenuInstaller>();
|
||||
installer.name = name;
|
||||
|
||||
toDestroy.Add(obj);
|
||||
|
||||
return installer;
|
||||
}
|
||||
|
||||
VRCExpressionsMenu.Control GenerateTestSubmenu(VRCExpressionsMenu menu)
|
||||
{
|
||||
var control = GenerateTestControl();
|
||||
control.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
||||
control.subMenu = menu;
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
VRCExpressionsMenu.Control GenerateTestControl()
|
||||
{
|
||||
var control = new VRCExpressionsMenu.Control();
|
||||
|
||||
VRCExpressionsMenu.Control.ControlType[] types = new[]
|
||||
{
|
||||
VRCExpressionsMenu.Control.ControlType.Button,
|
||||
// VRCExpressionsMenu.Control.ControlType.SubMenu,
|
||||
VRCExpressionsMenu.Control.ControlType.Toggle,
|
||||
VRCExpressionsMenu.Control.ControlType.RadialPuppet,
|
||||
VRCExpressionsMenu.Control.ControlType.FourAxisPuppet,
|
||||
VRCExpressionsMenu.Control.ControlType.TwoAxisPuppet,
|
||||
};
|
||||
|
||||
control.type = types[Random.Range(0, types.Length)];
|
||||
control.name = "Test Control " + controlIndex++;
|
||||
control.parameter = new VRCExpressionsMenu.Control.Parameter();
|
||||
control.parameter.name = "Test Parameter " + GUID.Generate();
|
||||
control.icon = new Texture2D(1, 1);
|
||||
control.labels = new[]
|
||||
{
|
||||
new VRCExpressionsMenu.Control.Label()
|
||||
{
|
||||
name = "label",
|
||||
icon = testTex
|
||||
}
|
||||
};
|
||||
control.subParameters = new[]
|
||||
{
|
||||
new VRCExpressionsMenu.Control.Parameter()
|
||||
{
|
||||
name = "Test Sub Parameter " + GUID.Generate()
|
||||
}
|
||||
};
|
||||
control.value = 0.42f;
|
||||
control.style = VRCExpressionsMenu.Control.Style.Style3;
|
||||
|
||||
return control;
|
||||
}
|
||||
|
||||
void AssertControlEquals(VRCExpressionsMenu.Control expected, VRCExpressionsMenu.Control actual)
|
||||
{
|
||||
Assert.AreEqual(expected.type, actual.type);
|
||||
Assert.AreEqual(expected.name, actual.name);
|
||||
Assert.AreEqual(expected.parameter.name, actual.parameter.name);
|
||||
Assert.AreNotSame(expected.parameter, actual.parameter);
|
||||
Assert.AreEqual(expected.icon, actual.icon);
|
||||
Assert.AreEqual(expected.labels.Length, actual.labels.Length);
|
||||
Assert.AreEqual(expected.labels[0].name, actual.labels[0].name);
|
||||
Assert.AreEqual(expected.labels[0].icon, actual.labels[0].icon);
|
||||
Assert.AreEqual(expected.subParameters.Length, actual.subParameters.Length);
|
||||
Assert.AreEqual(expected.subParameters[0].name, actual.subParameters[0].name);
|
||||
Assert.AreNotSame(expected.subParameters[0], actual.subParameters[0]);
|
||||
Assert.AreEqual(expected.value, actual.value);
|
||||
Assert.AreEqual(expected.style, actual.style);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c96564b3caa04c63aa9c7c4ce05429eb
|
||||
timeCreated: 1676981475
|
@ -11,6 +11,7 @@
|
||||
"com.unity.ugui": "1.0.0",
|
||||
"com.unity.xr.oculus.standalone": "2.38.4",
|
||||
"com.unity.xr.openvr.standalone": "2.0.5",
|
||||
"de.thryrallo.vrc.avatar-performance-tools": "https://github.com/Thryrallo/VRC-Avatar-Performance-Tools.git",
|
||||
"nadena.dev.modular-avatar": "0.0.1",
|
||||
"com.unity.modules.ai": "1.0.0",
|
||||
"com.unity.modules.androidjni": "1.0.0",
|
||||
|
@ -214,6 +214,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
ErrorReportUI.MaybeOpenErrorReportUI();
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
Resources.UnloadUnusedAssets();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
@ -13,6 +15,10 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
internal readonly AnimationDatabase AnimationDatabase = new AnimationDatabase();
|
||||
internal readonly AnimatorController AssetContainer;
|
||||
|
||||
internal readonly Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> ClonedMenus
|
||||
= new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
|
||||
|
||||
|
||||
public BuildContext(VRCAvatarDescriptor avatarDescriptor)
|
||||
{
|
||||
AvatarDescriptor = avatarDescriptor;
|
||||
@ -72,5 +78,38 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
merger.AddOverrideController("", overrideController, null);
|
||||
return merger.Finish();
|
||||
}
|
||||
|
||||
public VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu)
|
||||
{
|
||||
if (menu == null) return null;
|
||||
if (ClonedMenus.TryGetValue(menu, out var newMenu)) return newMenu;
|
||||
newMenu = Object.Instantiate(menu);
|
||||
this.SaveAsset(newMenu);
|
||||
ClonedMenus[menu] = newMenu;
|
||||
|
||||
foreach (var control in newMenu.controls)
|
||||
{
|
||||
if (Util.ValidateExpressionMenuIcon(control.icon) != Util.ValidateExpressionMenuIconResult.Success)
|
||||
control.icon = null;
|
||||
|
||||
for (int i = 0; i < control.labels.Length; i++)
|
||||
{
|
||||
var label = control.labels[i];
|
||||
var labelResult = Util.ValidateExpressionMenuIcon(label.icon);
|
||||
if (labelResult != Util.ValidateExpressionMenuIconResult.Success)
|
||||
{
|
||||
label.icon = null;
|
||||
control.labels[i] = label;
|
||||
}
|
||||
}
|
||||
|
||||
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
|
||||
{
|
||||
control.subMenu = CloneMenu(control.subMenu);
|
||||
}
|
||||
}
|
||||
|
||||
return newMenu;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal static class ClonedMenuMappings
|
||||
{
|
||||
/// <summary>
|
||||
/// Map to link the cloned menu from the clone source.
|
||||
/// If one menu is specified for multiple installers, they are replicated separately, so there is a one-to-many relationship.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<VRCExpressionsMenu, ImmutableList<VRCExpressionsMenu>> ClonedMappings =
|
||||
new Dictionary<VRCExpressionsMenu, ImmutableList<VRCExpressionsMenu>>();
|
||||
|
||||
/// <summary>
|
||||
/// Map to link the clone source from the cloned menu.
|
||||
/// Map is the opposite of ClonedMappings.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> OriginalMapping =
|
||||
new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
ClonedMappings.Clear();
|
||||
OriginalMapping.Clear();
|
||||
}
|
||||
|
||||
public static void Add(VRCExpressionsMenu original, VRCExpressionsMenu clonedMenu)
|
||||
{
|
||||
if (!ClonedMappings.TryGetValue(original, out ImmutableList<VRCExpressionsMenu> clonedMenus))
|
||||
{
|
||||
clonedMenus = ImmutableList<VRCExpressionsMenu>.Empty;
|
||||
}
|
||||
ClonedMappings[original] = clonedMenus.Add(clonedMenu);
|
||||
OriginalMapping[clonedMenu] = original;
|
||||
}
|
||||
|
||||
public static bool TryGetClonedMenus(VRCExpressionsMenu original, out ImmutableList<VRCExpressionsMenu> clonedMenus)
|
||||
{
|
||||
return ClonedMappings.TryGetValue(original, out clonedMenus);
|
||||
}
|
||||
|
||||
public static VRCExpressionsMenu GetOriginal(VRCExpressionsMenu cloned)
|
||||
{
|
||||
return OriginalMapping.TryGetValue(cloned, out VRCExpressionsMenu original) ? original : null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.modular_avatar.core;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
|
||||
@ -137,7 +138,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
|
||||
private static List<ErrorLog> CheckInternal(ModularAvatarMenuInstaller mi)
|
||||
{
|
||||
// TODO - check that target menu is in the avatar
|
||||
if (mi.menuToAppend == null)
|
||||
if (mi.menuToAppend == null && mi.GetComponent<MenuSourceComponent>() == null)
|
||||
{
|
||||
return new List<ErrorLog>()
|
||||
{
|
||||
|
@ -334,7 +334,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
|
||||
|
||||
internal static T ReportingObject<T>(UnityEngine.Object obj, Func<T> action)
|
||||
{
|
||||
CurrentReport._references.Push(obj);
|
||||
if (obj != null) CurrentReport._references.Push(obj);
|
||||
try
|
||||
{
|
||||
return action();
|
||||
@ -347,7 +347,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
|
||||
}
|
||||
finally
|
||||
{
|
||||
CurrentReport._references.Pop();
|
||||
if (obj != null) CurrentReport._references.Pop();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace nadena.dev.modular_avatar.editor.ErrorReporting
|
||||
{
|
||||
/// <summary>
|
||||
/// These exceptions will not be logged in the error report.
|
||||
/// </summary>
|
||||
public class NominalException : Exception
|
||||
{
|
||||
public NominalException()
|
||||
{
|
||||
}
|
||||
|
||||
protected NominalException([NotNull] SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
|
||||
public NominalException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public NominalException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 152d780db95c4e408240ec3cd4dad60e
|
||||
timeCreated: 1676979798
|
@ -1,180 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using VRC.SDKBase;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
class AvMenuTreeViewWindow : EditorWindow
|
||||
{
|
||||
private VRCAvatarDescriptor _avatarDescriptor;
|
||||
private AvMenuTreeView _treeView;
|
||||
|
||||
public VRCAvatarDescriptor Avatar
|
||||
{
|
||||
get => _treeView.Avatar;
|
||||
set => _treeView.Avatar = value;
|
||||
}
|
||||
|
||||
public ModularAvatarMenuInstaller TargetInstaller
|
||||
{
|
||||
get => _treeView.TargetInstaller;
|
||||
set => _treeView.TargetInstaller = value;
|
||||
}
|
||||
|
||||
public Action<VRCExpressionsMenu> OnMenuSelected = (menu) => { };
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_treeView = new AvMenuTreeView(new TreeViewState());
|
||||
_treeView.OnSelect = (menu) => OnMenuSelected.Invoke(menu);
|
||||
_treeView.OnDoubleclickSelect = Close;
|
||||
}
|
||||
|
||||
private void OnLostFocus()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OnMenuSelected = (menu) => { };
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (_treeView == null || _treeView.Avatar == null)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
_treeView.OnGUI(new Rect(0, 0, position.width, position.height));
|
||||
}
|
||||
|
||||
internal static void Show(VRCAvatarDescriptor Avatar, ModularAvatarMenuInstaller Installer, Action<VRCExpressionsMenu> OnSelect)
|
||||
{
|
||||
var window = GetWindow<AvMenuTreeViewWindow>();
|
||||
window.titleContent = new GUIContent("Select menu");
|
||||
|
||||
window.Avatar = Avatar;
|
||||
window.TargetInstaller = Installer;
|
||||
window.OnMenuSelected = OnSelect;
|
||||
|
||||
window.Show();
|
||||
}
|
||||
}
|
||||
|
||||
class AvMenuTreeView : TreeView
|
||||
{
|
||||
private VRCAvatarDescriptor _avatar;
|
||||
|
||||
public VRCAvatarDescriptor Avatar
|
||||
{
|
||||
get => _avatar;
|
||||
set
|
||||
{
|
||||
_avatar = value;
|
||||
Reload();
|
||||
}
|
||||
}
|
||||
|
||||
private ModularAvatarMenuInstaller _targetInstaller;
|
||||
|
||||
public ModularAvatarMenuInstaller TargetInstaller
|
||||
{
|
||||
get => _targetInstaller;
|
||||
set
|
||||
{
|
||||
_targetInstaller = value;
|
||||
Reload();
|
||||
}
|
||||
}
|
||||
|
||||
internal Action<VRCExpressionsMenu> OnSelect = (menu) => { };
|
||||
internal Action OnDoubleclickSelect = () => { };
|
||||
|
||||
private List<VRCExpressionsMenu> _menuItems = new List<VRCExpressionsMenu>();
|
||||
private HashSet<VRCExpressionsMenu> _visitedMenus = new HashSet<VRCExpressionsMenu>();
|
||||
|
||||
private MenuTree _menuTree;
|
||||
private Stack<VRCExpressionsMenu> _visitedMenuStack = new Stack<VRCExpressionsMenu>();
|
||||
|
||||
public AvMenuTreeView(TreeViewState state) : base(state)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void SelectionChanged(IList<int> selectedIds)
|
||||
{
|
||||
OnSelect.Invoke(_menuItems[selectedIds[0]]);
|
||||
}
|
||||
|
||||
protected override void DoubleClickedItem(int id)
|
||||
{
|
||||
OnSelect.Invoke(_menuItems[id]);
|
||||
OnDoubleclickSelect.Invoke();
|
||||
}
|
||||
|
||||
protected override TreeViewItem BuildRoot()
|
||||
{
|
||||
_menuItems.Clear();
|
||||
_visitedMenuStack.Clear();
|
||||
|
||||
_menuTree = new MenuTree(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>");
|
||||
List<TreeViewItem> treeItems = new List<TreeViewItem>
|
||||
{
|
||||
new TreeViewItem
|
||||
{
|
||||
id = 0,
|
||||
depth = 0,
|
||||
displayName = $"{Avatar.gameObject.name} ({(Avatar.expressionsMenu == null ? "None" : Avatar.expressionsMenu.name)})"
|
||||
}
|
||||
};
|
||||
_menuItems.Add(Avatar.expressionsMenu);
|
||||
_visitedMenuStack.Push(Avatar.expressionsMenu);
|
||||
|
||||
TraverseMenu(1, treeItems, Avatar.expressionsMenu);
|
||||
SetupParentsAndChildrenFromDepths(root, treeItems);
|
||||
return root;
|
||||
}
|
||||
|
||||
private void TraverseMenu(int depth, List<TreeViewItem> items, VRCExpressionsMenu menu)
|
||||
{
|
||||
IEnumerable<MenuTree.ChildElement> children = _menuTree.GetChildren(menu)
|
||||
.Where(child => !_visitedMenuStack.Contains(child.menu));
|
||||
foreach (MenuTree.ChildElement child in children)
|
||||
{
|
||||
if (child.menu == null) continue;
|
||||
string displayName = child.installer == null ?
|
||||
$"{child.menuName} ({child.menu.name})" :
|
||||
$"{child.menuName} ({child.menu.name}) InstallerObject : {child.installer.name}";
|
||||
items.Add(
|
||||
new TreeViewItem
|
||||
{
|
||||
id = items.Count,
|
||||
depth = depth,
|
||||
displayName = displayName
|
||||
}
|
||||
);
|
||||
_menuItems.Add(child.menu);
|
||||
_visitedMenuStack.Push(child.menu);
|
||||
TraverseMenu(depth + 1, items, child.menu);
|
||||
_visitedMenuStack.Pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c587475b37a44ad9ac97c6e7b9e1b4a2
|
||||
timeCreated: 1677148000
|
||||
folderAsset: yes
|
@ -0,0 +1,233 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using nadena.dev.modular_avatar.core.editor.menu;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using VRC.SDKBase;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
class AvMenuTreeViewWindow : EditorWindow
|
||||
{
|
||||
private VRCAvatarDescriptor _avatarDescriptor;
|
||||
private AvMenuTreeView _treeView;
|
||||
private long _cacheIndex = -1;
|
||||
|
||||
public VRCAvatarDescriptor Avatar
|
||||
{
|
||||
get => _treeView.Avatar;
|
||||
set => _treeView.Avatar = value;
|
||||
}
|
||||
|
||||
public ModularAvatarMenuInstaller TargetInstaller
|
||||
{
|
||||
get => _treeView.TargetInstaller;
|
||||
set => _treeView.TargetInstaller = value;
|
||||
}
|
||||
|
||||
public Action<object> OnMenuSelected = (menu) => { };
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_treeView = new AvMenuTreeView(new TreeViewState());
|
||||
_treeView.OnSelect = (menu) => OnMenuSelected.Invoke(menu);
|
||||
_treeView.OnDoubleclickSelect = Close;
|
||||
_cacheIndex = -1;
|
||||
}
|
||||
|
||||
private void OnLostFocus()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OnMenuSelected = (menu) => { };
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (_treeView == null || _treeView.Avatar == null)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cacheIndex != VirtualMenu.CacheSequence)
|
||||
{
|
||||
_treeView.ReloadPreservingExpanded();
|
||||
_cacheIndex = VirtualMenu.CacheSequence;
|
||||
}
|
||||
|
||||
_treeView.OnGUI(new Rect(0, 0, position.width, position.height));
|
||||
}
|
||||
|
||||
internal static void Show(VRCAvatarDescriptor Avatar, ModularAvatarMenuInstaller Installer,
|
||||
Action<object> OnSelect)
|
||||
{
|
||||
var window = GetWindow<AvMenuTreeViewWindow>();
|
||||
window.titleContent = new GUIContent("Select menu");
|
||||
|
||||
window.Avatar = Avatar;
|
||||
window.TargetInstaller = Installer;
|
||||
window.OnMenuSelected = OnSelect;
|
||||
|
||||
window.Show();
|
||||
}
|
||||
}
|
||||
|
||||
class AvMenuTreeView : TreeView
|
||||
{
|
||||
private VRCAvatarDescriptor _avatar;
|
||||
|
||||
public VRCAvatarDescriptor Avatar
|
||||
{
|
||||
get => _avatar;
|
||||
set
|
||||
{
|
||||
_avatar = value;
|
||||
Reload();
|
||||
}
|
||||
}
|
||||
|
||||
private ModularAvatarMenuInstaller _targetInstaller;
|
||||
|
||||
public ModularAvatarMenuInstaller TargetInstaller
|
||||
{
|
||||
get => _targetInstaller;
|
||||
set
|
||||
{
|
||||
_targetInstaller = value;
|
||||
Reload();
|
||||
}
|
||||
}
|
||||
|
||||
internal Action<object> OnSelect = (menu) => { };
|
||||
internal Action OnDoubleclickSelect = () => { };
|
||||
|
||||
private List<object> _nodeKeys = new List<object>();
|
||||
private HashSet<object> _visitedMenus = new HashSet<object>();
|
||||
|
||||
private VirtualMenu _menuTree;
|
||||
private Stack<object> _visitedMenuStack = new Stack<object>();
|
||||
|
||||
public AvMenuTreeView(TreeViewState state) : base(state)
|
||||
{
|
||||
}
|
||||
|
||||
public void ReloadPreservingExpanded()
|
||||
{
|
||||
var expanded = GetExpanded().Select(id => _nodeKeys[id]).ToImmutableHashSet();
|
||||
var selected = GetSelection().Select(id => _nodeKeys[id]).ToImmutableHashSet();
|
||||
CollapseAll();
|
||||
Reload();
|
||||
SetExpanded(Enumerable.Range(0, _nodeKeys.Count)
|
||||
.Where(i => _nodeKeys[i] != null && expanded.Contains(_nodeKeys[i]))
|
||||
.ToList());
|
||||
SetSelection(Enumerable.Range(0, _nodeKeys.Count)
|
||||
.Where(i => _nodeKeys[i] != null && selected.Contains(_nodeKeys[i]))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
protected override void SelectionChanged(IList<int> selectedIds)
|
||||
{
|
||||
OnSelect.Invoke(_nodeKeys[selectedIds[0]]);
|
||||
}
|
||||
|
||||
protected override void DoubleClickedItem(int id)
|
||||
{
|
||||
OnSelect.Invoke(_nodeKeys[id]);
|
||||
OnDoubleclickSelect.Invoke();
|
||||
}
|
||||
|
||||
protected override TreeViewItem BuildRoot()
|
||||
{
|
||||
_nodeKeys.Clear();
|
||||
_visitedMenuStack.Clear();
|
||||
|
||||
_menuTree = VirtualMenu.ForAvatar(_avatar);
|
||||
|
||||
var preferredRoot = FindPreferredRoot(_menuTree);
|
||||
var rootName = "";
|
||||
if (preferredRoot is ModularAvatarMenuGroup group)
|
||||
{
|
||||
if (group.targetObject != null) rootName = $"({group.targetObject.name})";
|
||||
else rootName = $"({group.gameObject.name})";
|
||||
}
|
||||
else if (preferredRoot is VRCExpressionsMenu menu)
|
||||
{
|
||||
rootName = $"({menu.name})";
|
||||
}
|
||||
|
||||
var root = new TreeViewItem(-1, -1, "<root>");
|
||||
List<TreeViewItem> treeItems = new List<TreeViewItem>
|
||||
{
|
||||
new TreeViewItem
|
||||
{
|
||||
id = 0,
|
||||
depth = 0,
|
||||
displayName =
|
||||
$"{Avatar.gameObject.name} {rootName}"
|
||||
}
|
||||
};
|
||||
|
||||
_nodeKeys.Add(preferredRoot);
|
||||
_visitedMenuStack.Push(preferredRoot);
|
||||
TraverseMenu(1, treeItems, _menuTree.RootMenuNode);
|
||||
SetupParentsAndChildrenFromDepths(root, treeItems);
|
||||
return root;
|
||||
}
|
||||
|
||||
private object FindPreferredRoot(VirtualMenu menuTree)
|
||||
{
|
||||
// There's always a VRCExpressionsMenu at the root, but we'd prefer to add stuff under a MenuItem tree if
|
||||
// available. See if we can find one.
|
||||
foreach (var installer in _avatar.GetComponentsInChildren<ModularAvatarMenuInstaller>(true))
|
||||
{
|
||||
if (installer.installTargetMenu != null && installer.installTargetMenu != menuTree.RootMenuKey)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var menuSource = installer.GetComponent<MenuSource>();
|
||||
if (menuSource == null || !(menuSource is ModularAvatarMenuGroup group)) continue;
|
||||
|
||||
return menuSource;
|
||||
}
|
||||
|
||||
return menuTree.RootMenuKey;
|
||||
}
|
||||
|
||||
private void TraverseMenu(int depth, List<TreeViewItem> items, VirtualMenuNode node)
|
||||
{
|
||||
IEnumerable<VirtualControl> children = node.Controls
|
||||
.Where(control => control.type == VRCExpressionsMenu.Control.ControlType.SubMenu &&
|
||||
control.SubmenuNode != null &&
|
||||
!_visitedMenuStack.Contains(control.SubmenuNode));
|
||||
foreach (var child in children)
|
||||
{
|
||||
string displayName = child.name;
|
||||
|
||||
items.Add(
|
||||
new TreeViewItem
|
||||
{
|
||||
id = items.Count,
|
||||
depth = depth,
|
||||
displayName = displayName
|
||||
}
|
||||
);
|
||||
_nodeKeys.Add(child.SubmenuNode.NodeKey);
|
||||
_visitedMenuStack.Push(child.SubmenuNode);
|
||||
TraverseMenu(depth + 1, items, child.SubmenuNode);
|
||||
_visitedMenuStack.Pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
using UnityEditor;
|
||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
[CustomEditor(typeof(ModularAvatarMenuItem))]
|
||||
[CanEditMultipleObjects]
|
||||
internal class MAMenuItemInspector : MAEditorBase
|
||||
{
|
||||
private MenuItemCoreGUI _coreGUI;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
_coreGUI = new MenuItemCoreGUI(serializedObject, Repaint);
|
||||
_coreGUI.AlwaysExpandContents = true;
|
||||
}
|
||||
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
_coreGUI.DoGUI();
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
ShowLanguageUI();
|
||||
}
|
||||
}
|
||||
|
||||
[CustomEditor(typeof(ModularAvatarMenuGroup))]
|
||||
internal class MAMenuGroupInspector : MAEditorBase
|
||||
{
|
||||
private MenuPreviewGUI _previewGUI;
|
||||
private SerializedProperty _prop_target;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
_previewGUI = new MenuPreviewGUI(Repaint);
|
||||
_prop_target = serializedObject.FindProperty(nameof(ModularAvatarMenuGroup.targetObject));
|
||||
}
|
||||
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
EditorGUILayout.PropertyField(_prop_target, G("menuitem.prop.source_override"));
|
||||
|
||||
_previewGUI.DoGUI((ModularAvatarMenuGroup) target);
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
ShowLanguageUI();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b674c72186c4e6884b0cd05098f11b6
|
||||
timeCreated: 1676791017
|
@ -0,0 +1,565 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using nadena.dev.modular_avatar.core.editor.menu;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||
using static nadena.dev.modular_avatar.core.editor.Util;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
[CustomEditor(typeof(ModularAvatarMenuInstaller))]
|
||||
[CanEditMultipleObjects]
|
||||
internal class MenuInstallerEditor : MAEditorBase
|
||||
{
|
||||
private ModularAvatarMenuInstaller _installer;
|
||||
private Editor _innerMenuEditor;
|
||||
private VRCExpressionsMenu _menuToAppend;
|
||||
|
||||
private bool _menuFoldout;
|
||||
private bool _devFoldout;
|
||||
|
||||
private MenuPreviewGUI _previewGUI;
|
||||
|
||||
private HashSet<VRCExpressionsMenu> _avatarMenus;
|
||||
|
||||
private Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>> _menuInstallersMap;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_installer = (ModularAvatarMenuInstaller) target;
|
||||
_previewGUI = new MenuPreviewGUI(Repaint);
|
||||
|
||||
FindMenus();
|
||||
FindMenuInstallers();
|
||||
|
||||
VRCAvatarDescriptor commonAvatar = FindCommonAvatar();
|
||||
}
|
||||
|
||||
private long _cacheSeq = -1;
|
||||
private ImmutableList<object> _cachedTargets = null;
|
||||
|
||||
// Interpretation:
|
||||
// <empty> : Inconsistent install targets
|
||||
// List of [null]: Install to root
|
||||
// List of [VRCExpMenu]: Install to expressions menu
|
||||
// List of [InstallTarget]: Install to single install target
|
||||
// List of [InstallTarget, InstallTarget ...]: Install to multiple install targets
|
||||
private ImmutableList<object> InstallTargets
|
||||
{
|
||||
get
|
||||
{
|
||||
if (VirtualMenu.CacheSequence == _cacheSeq && _cachedTargets != null) return _cachedTargets;
|
||||
|
||||
List<ImmutableList<object>> perTarget = new List<ImmutableList<object>>();
|
||||
|
||||
var commonAvatar = FindCommonAvatar();
|
||||
if (commonAvatar == null)
|
||||
{
|
||||
_cacheSeq = VirtualMenu.CacheSequence;
|
||||
_cachedTargets = ImmutableList<object>.Empty;
|
||||
return _cachedTargets;
|
||||
}
|
||||
|
||||
var virtualMenu = VirtualMenu.ForAvatar(commonAvatar);
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var installer = (ModularAvatarMenuInstaller) target;
|
||||
|
||||
var installTargets = virtualMenu.GetInstallTargetsForInstaller(installer)
|
||||
.Select(o => (object) o).ToImmutableList();
|
||||
if (installTargets.Any())
|
||||
{
|
||||
perTarget.Add(installTargets);
|
||||
}
|
||||
else
|
||||
{
|
||||
perTarget.Add(ImmutableList<object>.Empty.Add(installer.installTargetMenu));
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i < perTarget.Count; i++)
|
||||
{
|
||||
if (perTarget[0].Count != perTarget[i].Count ||
|
||||
perTarget[0].Zip(perTarget[i], (a, b) => (Resolve(a) != Resolve(b))).Any(differs => differs))
|
||||
{
|
||||
perTarget.Clear();
|
||||
perTarget.Add(ImmutableList<object>.Empty);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_cacheSeq = VirtualMenu.CacheSequence;
|
||||
_cachedTargets = perTarget[0];
|
||||
return _cachedTargets;
|
||||
|
||||
object Resolve(object p0)
|
||||
{
|
||||
if (p0 is ModularAvatarMenuInstallTarget target && target != null) return target.transform.parent;
|
||||
return p0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupMenuEditor()
|
||||
{
|
||||
if (targets.Length != 1)
|
||||
{
|
||||
_innerMenuEditor = null;
|
||||
_menuToAppend = null;
|
||||
}
|
||||
else if (_installer.menuToAppend != _menuToAppend)
|
||||
{
|
||||
if (_installer.menuToAppend == null) _innerMenuEditor = null;
|
||||
else
|
||||
{
|
||||
_innerMenuEditor = CreateEditor(_installer.menuToAppend);
|
||||
}
|
||||
|
||||
_menuToAppend = _installer.menuToAppend;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
SetupMenuEditor();
|
||||
|
||||
var installTo = serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.installTargetMenu));
|
||||
|
||||
var isEnabled = targets.Length != 1 || ((ModularAvatarMenuInstaller) target).enabled;
|
||||
|
||||
VRCAvatarDescriptor commonAvatar = FindCommonAvatar();
|
||||
|
||||
if (InstallTargets.Count == 0)
|
||||
{
|
||||
// TODO - show warning for inconsistent targets?
|
||||
}
|
||||
else if (InstallTargets.Count > 0)
|
||||
{
|
||||
if (InstallTargets.Count == 1)
|
||||
{
|
||||
if (InstallTargets[0] == null)
|
||||
{
|
||||
if (isEnabled)
|
||||
{
|
||||
EditorGUILayout.HelpBox(S("menuinstall.help.hint_set_menu"), MessageType.Info);
|
||||
}
|
||||
}
|
||||
else if (InstallTargets[0] is VRCExpressionsMenu menu
|
||||
&& !IsMenuReachable(RuntimeUtil.FindAvatarInParents(((Component) target).transform), menu))
|
||||
{
|
||||
EditorGUILayout.HelpBox(S("menuinstall.help.hint_bad_menu"), MessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
if (InstallTargets.Count == 1 && (InstallTargets[0] is VRCExpressionsMenu || InstallTargets[0] == null))
|
||||
{
|
||||
var displayValue = installTo.objectReferenceValue;
|
||||
if (displayValue == null) displayValue = commonAvatar.expressionsMenu;
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var newValue = EditorGUILayout.ObjectField(G("menuinstall.installto"), displayValue,
|
||||
typeof(VRCExpressionsMenu), false);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
installTo.objectReferenceValue = newValue;
|
||||
_cacheSeq = -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
foreach (var target in InstallTargets)
|
||||
{
|
||||
if (target is VRCExpressionsMenu menu)
|
||||
{
|
||||
EditorGUILayout.ObjectField(G("menuinstall.installto"), menu,
|
||||
typeof(VRCExpressionsMenu), true);
|
||||
}
|
||||
else if (target is ModularAvatarMenuInstallTarget t)
|
||||
{
|
||||
EditorGUILayout.ObjectField(G("menuinstall.installto"), t.transform.parent.gameObject,
|
||||
typeof(GameObject), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var avatar = commonAvatar;
|
||||
if (avatar != null && InstallTargets.Count == 1 && GUILayout.Button(G("menuinstall.selectmenu")))
|
||||
{
|
||||
AvMenuTreeViewWindow.Show(avatar, _installer, menu =>
|
||||
{
|
||||
if (InstallTargets.Count != 1 || menu == InstallTargets[0]) return;
|
||||
|
||||
if (InstallTargets[0] is ModularAvatarMenuInstallTarget oldTarget && oldTarget != null)
|
||||
{
|
||||
DestroyInstallTargets();
|
||||
}
|
||||
|
||||
if (menu is ModularAvatarMenuItem item)
|
||||
{
|
||||
if (item.MenuSource == SubmenuSource.MenuAsset)
|
||||
{
|
||||
menu = item.Control.subMenu;
|
||||
}
|
||||
else
|
||||
{
|
||||
var menuParent = item.menuSource_otherObjectChildren != null
|
||||
? item.menuSource_otherObjectChildren
|
||||
: item.gameObject;
|
||||
|
||||
menu = new MenuNodesUnder(menuParent);
|
||||
}
|
||||
}
|
||||
else if (menu is ModularAvatarMenuGroup group)
|
||||
{
|
||||
if (group.targetObject != null) menu = new MenuNodesUnder(group.targetObject);
|
||||
else menu = new MenuNodesUnder(group.gameObject);
|
||||
}
|
||||
|
||||
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 MenuNodesUnder nodesUnder)
|
||||
{
|
||||
installTo.objectReferenceValue = null;
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var installer = (ModularAvatarMenuInstaller) target;
|
||||
var child = new GameObject();
|
||||
Undo.RegisterCreatedObjectUndo(child, "Set install target");
|
||||
child.transform.SetParent(nodesUnder.root.transform, false);
|
||||
child.name = installer.gameObject.name;
|
||||
|
||||
var targetComponent = child.AddComponent<ModularAvatarMenuInstallTarget>();
|
||||
targetComponent.installer = installer;
|
||||
|
||||
EditorGUIUtility.PingObject(child);
|
||||
}
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
VirtualMenu.InvalidateCaches();
|
||||
Repaint();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (targets.Length == 1)
|
||||
{
|
||||
_menuFoldout = EditorGUILayout.Foldout(_menuFoldout, G("menuinstall.showcontents"));
|
||||
if (_menuFoldout)
|
||||
{
|
||||
_previewGUI.DoGUI((ModularAvatarMenuInstaller) target);
|
||||
}
|
||||
}
|
||||
|
||||
if (targets.Any(t =>
|
||||
{
|
||||
var installer = (ModularAvatarMenuInstaller) t;
|
||||
return installer.GetComponent<MenuSource>() == null && installer.menuToAppend != null;
|
||||
}))
|
||||
{
|
||||
if (GUILayout.Button("Extract menu to objects"))
|
||||
{
|
||||
ExtractMenu();
|
||||
}
|
||||
}
|
||||
|
||||
bool inconsistentSources = false;
|
||||
MenuSource menuSource = null;
|
||||
bool first = true;
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var component = (ModularAvatarMenuInstaller) target;
|
||||
var componentSource = component.GetComponent<MenuSource>();
|
||||
if (componentSource != null)
|
||||
{
|
||||
if (menuSource == null && first)
|
||||
{
|
||||
menuSource = componentSource;
|
||||
}
|
||||
else
|
||||
{
|
||||
inconsistentSources = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (menuSource != null)
|
||||
{
|
||||
// TODO localize
|
||||
EditorGUILayout.HelpBox("Menu contents provided by " + menuSource.GetType() + " component",
|
||||
MessageType.Info);
|
||||
}
|
||||
|
||||
if (!inconsistentSources)
|
||||
{
|
||||
_devFoldout = EditorGUILayout.Foldout(_devFoldout, G("menuinstall.devoptions"));
|
||||
if (_devFoldout)
|
||||
{
|
||||
SerializedProperty menuToAppendProperty =
|
||||
serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.menuToAppend));
|
||||
if (!menuToAppendProperty.hasMultipleDifferentValues)
|
||||
{
|
||||
switch (ValidateExpressionMenuIcon(
|
||||
(VRCExpressionsMenu) menuToAppendProperty.objectReferenceValue))
|
||||
{
|
||||
case ValidateExpressionMenuIconResult.Success:
|
||||
break;
|
||||
case ValidateExpressionMenuIconResult.TooLarge:
|
||||
EditorGUILayout.HelpBox(S("menuinstall.menu_icon_too_large"), MessageType.Error);
|
||||
break;
|
||||
case ValidateExpressionMenuIconResult.Uncompressed:
|
||||
EditorGUILayout.HelpBox(S("menuinstall.menu_icon_uncompressed"), MessageType.Error);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.PropertyField(
|
||||
menuToAppendProperty, new GUIContent(G("menuinstall.srcmenu")));
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
Localization.ShowLanguageUI();
|
||||
}
|
||||
|
||||
private void ExtractMenu()
|
||||
{
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
foreach (var t in targets)
|
||||
{
|
||||
var installer = (ModularAvatarMenuInstaller) t;
|
||||
if (installer.GetComponent<MenuSourceComponent>() || installer.menuToAppend == null) continue;
|
||||
|
||||
var menu = installer.menuToAppend;
|
||||
if (menu.controls.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Undo.RecordObject(installer, "Extract menu");
|
||||
|
||||
if (menu.controls.Count == 1)
|
||||
{
|
||||
// Attach control directly to the installer
|
||||
var item = installer.gameObject.AddComponent<ModularAvatarMenuItem>();
|
||||
Undo.RegisterCreatedObjectUndo(item, "Extract menu");
|
||||
MenuExtractor.ControlToMenuItem(item, menu.controls[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use a menu group and attach items on a child
|
||||
var group = installer.gameObject.AddComponent<ModularAvatarMenuGroup>();
|
||||
var menuRoot = new GameObject();
|
||||
menuRoot.name = "Menu";
|
||||
Undo.RegisterCreatedObjectUndo(menuRoot, "Extract menu");
|
||||
menuRoot.transform.SetParent(group.transform, false);
|
||||
foreach (var control in menu.controls)
|
||||
{
|
||||
var itemObject = new GameObject();
|
||||
itemObject.gameObject.name = control.name;
|
||||
Undo.RegisterCreatedObjectUndo(itemObject, "Extract menu");
|
||||
itemObject.transform.SetParent(menuRoot.transform, false);
|
||||
var item = itemObject.AddComponent<ModularAvatarMenuItem>();
|
||||
MenuExtractor.ControlToMenuItem(item, control);
|
||||
}
|
||||
}
|
||||
|
||||
PrefabUtility.RecordPrefabInstancePropertyModifications(installer);
|
||||
EditorUtility.SetDirty(installer);
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyInstallTargets()
|
||||
{
|
||||
VirtualMenu menu = VirtualMenu.ForAvatar(FindCommonAvatar());
|
||||
|
||||
foreach (var t in targets)
|
||||
{
|
||||
foreach (var oldTarget in menu.GetInstallTargetsForInstaller((ModularAvatarMenuInstaller) t))
|
||||
{
|
||||
if (PrefabUtility.IsPartOfPrefabInstance(oldTarget))
|
||||
{
|
||||
Undo.RecordObject(oldTarget, "Change menu install target");
|
||||
oldTarget.installer = null;
|
||||
PrefabUtility.RecordPrefabInstancePropertyModifications(oldTarget);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (oldTarget.transform.childCount == 0 &&
|
||||
oldTarget.GetComponents(typeof(Component)).Length == 2)
|
||||
{
|
||||
Undo.DestroyObjectImmediate(oldTarget.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Undo.DestroyObjectImmediate(oldTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private VRCAvatarDescriptor FindCommonAvatar()
|
||||
{
|
||||
VRCAvatarDescriptor commonAvatar = null;
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var component = (ModularAvatarMenuInstaller) target;
|
||||
var avatar = RuntimeUtil.FindAvatarInParents(component.transform);
|
||||
if (avatar == null) return null;
|
||||
|
||||
if (commonAvatar == null)
|
||||
{
|
||||
commonAvatar = avatar;
|
||||
}
|
||||
else if (commonAvatar != avatar)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return commonAvatar;
|
||||
}
|
||||
|
||||
private void FindMenus()
|
||||
{
|
||||
if (targets.Length > 1)
|
||||
{
|
||||
_avatarMenus = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_avatarMenus = new HashSet<VRCExpressionsMenu>();
|
||||
var queue = new Queue<VRCExpressionsMenu>();
|
||||
var avatar = RuntimeUtil.FindAvatarInParents(((Component) target).transform);
|
||||
if (avatar == null || avatar.expressionsMenu == null) return;
|
||||
queue.Enqueue(avatar.expressionsMenu);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var menu = queue.Dequeue();
|
||||
if (_avatarMenus.Contains(menu)) continue;
|
||||
|
||||
_avatarMenus.Add(menu);
|
||||
foreach (var subMenu in menu.controls)
|
||||
{
|
||||
if (subMenu.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
|
||||
{
|
||||
queue.Enqueue(subMenu.subMenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FindMenuInstallers()
|
||||
{
|
||||
if (targets.Length > 1)
|
||||
{
|
||||
_menuInstallersMap = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_menuInstallersMap = new Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>>();
|
||||
var avatar = RuntimeUtil.FindAvatarInParents(((Component) target).transform);
|
||||
if (avatar == null) return;
|
||||
var menuInstallers = avatar.GetComponentsInChildren<ModularAvatarMenuInstaller>(true)
|
||||
.Where(menuInstaller => menuInstaller.enabled && menuInstaller.menuToAppend != null);
|
||||
foreach (ModularAvatarMenuInstaller menuInstaller in menuInstallers)
|
||||
{
|
||||
if (menuInstaller == target) continue;
|
||||
var visitedMenus = new HashSet<VRCExpressionsMenu>();
|
||||
var queue = new Queue<VRCExpressionsMenu>();
|
||||
queue.Enqueue(menuInstaller.menuToAppend);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
VRCExpressionsMenu parent = queue.Dequeue();
|
||||
var controls = parent.controls.Where(control =>
|
||||
control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && control.subMenu != null);
|
||||
foreach (VRCExpressionsMenu.Control control in controls)
|
||||
{
|
||||
// Do not filter in LINQ to avoid closure allocation
|
||||
if (visitedMenus.Contains(control.subMenu)) continue;
|
||||
if (!_menuInstallersMap.TryGetValue(control.subMenu,
|
||||
out List<ModularAvatarMenuInstaller> fromInstallers))
|
||||
{
|
||||
fromInstallers = new List<ModularAvatarMenuInstaller>();
|
||||
_menuInstallersMap[control.subMenu] = fromInstallers;
|
||||
}
|
||||
|
||||
fromInstallers.Add(menuInstaller);
|
||||
visitedMenus.Add(control.subMenu);
|
||||
queue.Enqueue(control.subMenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsMenuReachable(VRCAvatarDescriptor avatar, VRCExpressionsMenu menu)
|
||||
{
|
||||
var virtualMenu = VirtualMenu.ForAvatar(avatar);
|
||||
|
||||
return virtualMenu.ContainsMenu(menu);
|
||||
}
|
||||
|
||||
private static ValidateExpressionMenuIconResult ValidateExpressionMenuIcon(VRCExpressionsMenu menu,
|
||||
HashSet<VRCExpressionsMenu> visitedMenus = null)
|
||||
{
|
||||
if (menu == null) return ValidateExpressionMenuIconResult.Success;
|
||||
if (visitedMenus == null) visitedMenus = new HashSet<VRCExpressionsMenu>();
|
||||
if (visitedMenus.Contains(menu)) return ValidateExpressionMenuIconResult.Success;
|
||||
visitedMenus.Add(menu);
|
||||
|
||||
foreach (VRCExpressionsMenu.Control control in menu.controls)
|
||||
{
|
||||
// Control
|
||||
ValidateExpressionMenuIconResult result = Util.ValidateExpressionMenuIcon(control.icon);
|
||||
if (result != ValidateExpressionMenuIconResult.Success) return result;
|
||||
|
||||
// Labels
|
||||
if (control.labels != null)
|
||||
{
|
||||
foreach (VRCExpressionsMenu.Control.Label label in control.labels)
|
||||
{
|
||||
ValidateExpressionMenuIconResult labelResult = Util.ValidateExpressionMenuIcon(label.icon);
|
||||
if (labelResult != ValidateExpressionMenuIconResult.Success) return labelResult;
|
||||
}
|
||||
}
|
||||
|
||||
// SubMenu
|
||||
if (control.type != VRCExpressionsMenu.Control.ControlType.SubMenu) continue;
|
||||
ValidateExpressionMenuIconResult subMenuResult =
|
||||
ValidateExpressionMenuIcon(control.subMenu, visitedMenus);
|
||||
if (subMenuResult != ValidateExpressionMenuIconResult.Success) return subMenuResult;
|
||||
}
|
||||
|
||||
return ValidateExpressionMenuIconResult.Success;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,444 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using Codice.CM.Common.Tree.Partial;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(SubmenuSource))]
|
||||
class SubmenuSourceDrawer : EnumDrawer<SubmenuSource>
|
||||
{
|
||||
protected override string localizationPrefix => "submenu_source";
|
||||
}
|
||||
|
||||
internal class MenuItemCoreGUI
|
||||
{
|
||||
private static readonly ObjectIDGenerator IdGenerator = new ObjectIDGenerator();
|
||||
private readonly GameObject _parameterReference;
|
||||
private readonly Action _redraw;
|
||||
|
||||
private readonly SerializedObject _obj;
|
||||
|
||||
private readonly SerializedProperty _name;
|
||||
private readonly SerializedProperty _texture;
|
||||
private readonly SerializedProperty _type;
|
||||
private readonly SerializedProperty _value;
|
||||
private readonly SerializedProperty _submenu;
|
||||
|
||||
private readonly ParameterGUI _parameterGUI;
|
||||
|
||||
private readonly SerializedProperty _subParamsRoot;
|
||||
private readonly SerializedProperty _labelsRoot;
|
||||
|
||||
private readonly MenuPreviewGUI _previewGUI;
|
||||
|
||||
private ParameterGUI[] _subParams;
|
||||
private SerializedProperty[] _labels;
|
||||
|
||||
private int texPicker = -1;
|
||||
|
||||
private readonly SerializedProperty _prop_submenuSource;
|
||||
private readonly SerializedProperty _prop_otherObjSource;
|
||||
|
||||
public bool AlwaysExpandContents = false;
|
||||
public bool ExpandContents = false;
|
||||
|
||||
public MenuItemCoreGUI(SerializedObject obj, Action redraw)
|
||||
{
|
||||
_obj = obj;
|
||||
|
||||
GameObject parameterReference = null;
|
||||
if (obj.targetObjects.Length == 1)
|
||||
{
|
||||
parameterReference = (obj.targetObject as Component)?.gameObject;
|
||||
}
|
||||
|
||||
_parameterReference = parameterReference;
|
||||
_redraw = redraw;
|
||||
|
||||
var gameObjects = new SerializedObject(
|
||||
obj.targetObjects.Select(o =>
|
||||
(UnityEngine.Object) ((ModularAvatarMenuItem) o).gameObject
|
||||
).ToArray()
|
||||
);
|
||||
|
||||
_name = gameObjects.FindProperty("m_Name");
|
||||
var control = obj.FindProperty(nameof(ModularAvatarMenuItem.Control));
|
||||
|
||||
_texture = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
|
||||
_type = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.type));
|
||||
var parameter = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
|
||||
.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter.name));
|
||||
_value = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
|
||||
_submenu = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subMenu));
|
||||
|
||||
_parameterGUI = new ParameterGUI(parameterReference, parameter, redraw);
|
||||
|
||||
_subParamsRoot = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters));
|
||||
_labelsRoot = control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.labels));
|
||||
|
||||
_prop_submenuSource = obj.FindProperty(nameof(ModularAvatarMenuItem.MenuSource));
|
||||
_prop_otherObjSource = obj.FindProperty(nameof(ModularAvatarMenuItem.menuSource_otherObjectChildren));
|
||||
_previewGUI = new MenuPreviewGUI(redraw);
|
||||
}
|
||||
|
||||
public MenuItemCoreGUI(GameObject parameterReference, SerializedProperty _control, Action redraw)
|
||||
{
|
||||
_obj = _control.serializedObject;
|
||||
_parameterReference = parameterReference;
|
||||
_redraw = redraw;
|
||||
_name = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.name));
|
||||
_texture = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.icon));
|
||||
_type = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.type));
|
||||
var parameter = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter))
|
||||
.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.parameter.name));
|
||||
_value = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.value));
|
||||
_submenu = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subMenu));
|
||||
|
||||
_parameterGUI = new ParameterGUI(parameterReference, parameter, redraw);
|
||||
|
||||
_subParamsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subParameters));
|
||||
_labelsRoot = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.labels));
|
||||
|
||||
_prop_submenuSource = null;
|
||||
_prop_otherObjSource = null;
|
||||
_previewGUI = new MenuPreviewGUI(redraw);
|
||||
}
|
||||
|
||||
public void DoGUI()
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
|
||||
EditorGUILayout.BeginVertical();
|
||||
EditorGUILayout.PropertyField(_name, G("menuitem.prop.name"));
|
||||
EditorGUILayout.PropertyField(_texture, G("menuitem.prop.icon"));
|
||||
EditorGUILayout.PropertyField(_type, G("menuitem.prop.type"));
|
||||
EditorGUILayout.PropertyField(_value, G("menuitem.prop.value"));
|
||||
|
||||
_parameterGUI.DoGUI();
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
if (_texture != null)
|
||||
{
|
||||
var tex = _texture.objectReferenceValue as Texture2D;
|
||||
if (tex != null && !_texture.hasMultipleDifferentValues)
|
||||
{
|
||||
var size = EditorGUIUtility.singleLineHeight * 5;
|
||||
var margin = 4;
|
||||
var withMargin = new Vector2(margin + size, margin + size);
|
||||
|
||||
var rect = GUILayoutUtility.GetRect(withMargin.x, withMargin.y, GUILayout.ExpandWidth(false),
|
||||
GUILayout.ExpandHeight(true));
|
||||
rect.x += margin;
|
||||
rect.y = rect.y + rect.height / 2 - size / 2;
|
||||
rect.width = size;
|
||||
rect.height = size;
|
||||
|
||||
GUI.Box(rect, new GUIContent(), "flow node 1");
|
||||
GUI.DrawTexture(rect, tex);
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
try
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.BeginVertical();
|
||||
|
||||
if (_type.hasMultipleDifferentValues) return;
|
||||
VRCExpressionsMenu.Control.ControlType type =
|
||||
(VRCExpressionsMenu.Control.ControlType) Enum
|
||||
.GetValues(typeof(VRCExpressionsMenu.Control.ControlType))
|
||||
.GetValue(_type.enumValueIndex);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case VRCExpressionsMenu.Control.ControlType.Button:
|
||||
case VRCExpressionsMenu.Control.ControlType.Toggle:
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case VRCExpressionsMenu.Control.ControlType.SubMenu:
|
||||
{
|
||||
object menuSource = null;
|
||||
|
||||
if (_prop_submenuSource != null)
|
||||
{
|
||||
EditorGUILayout.PropertyField(_prop_submenuSource, G("menuitem.prop.submenu_source"));
|
||||
if (_prop_submenuSource.hasMultipleDifferentValues) break;
|
||||
|
||||
var sourceType = (SubmenuSource) Enum.GetValues(typeof(SubmenuSource))
|
||||
.GetValue(_prop_submenuSource.enumValueIndex);
|
||||
|
||||
switch (sourceType)
|
||||
{
|
||||
case SubmenuSource.Children:
|
||||
{
|
||||
EditorGUILayout.PropertyField(_prop_otherObjSource,
|
||||
G("menuitem.prop.source_override"));
|
||||
if (_prop_otherObjSource.hasMultipleDifferentValues) break;
|
||||
if (_prop_otherObjSource.objectReferenceValue == null)
|
||||
{
|
||||
if (_obj.targetObjects.Length != 1) break;
|
||||
menuSource = new MenuNodesUnder((_obj.targetObject as Component)?.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
menuSource =
|
||||
new MenuNodesUnder((GameObject) _prop_otherObjSource.objectReferenceValue);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case SubmenuSource.MenuAsset:
|
||||
{
|
||||
EditorGUILayout.PropertyField(_submenu, G("menuitem.prop.submenu_asset"));
|
||||
if (_submenu.hasMultipleDifferentValues) break;
|
||||
menuSource = _submenu.objectReferenceValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Native VRCSDK control
|
||||
EditorGUILayout.PropertyField(_submenu, G("menuitem.prop.submenu_asset"));
|
||||
if (_submenu.hasMultipleDifferentValues) break;
|
||||
menuSource = _submenu.objectReferenceValue;
|
||||
}
|
||||
|
||||
if (menuSource != null)
|
||||
{
|
||||
if (AlwaysExpandContents)
|
||||
{
|
||||
ExpandContents = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.indentLevel += 1;
|
||||
ExpandContents = EditorGUILayout.Foldout(ExpandContents, G("menuitem.showcontents"));
|
||||
EditorGUI.indentLevel -= 1;
|
||||
}
|
||||
|
||||
if (ExpandContents)
|
||||
{
|
||||
if (menuSource is VRCExpressionsMenu menu) _previewGUI.DoGUI(menu, _parameterReference);
|
||||
else if (menuSource is MenuSource nodes) _previewGUI.DoGUI(nodes);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case VRCExpressionsMenu.Control.ControlType.RadialPuppet:
|
||||
{
|
||||
EnsureParameterCount(1);
|
||||
|
||||
_subParams[0].DoGUI(G("menuitem.param.rotation"));
|
||||
|
||||
break;
|
||||
}
|
||||
case VRCExpressionsMenu.Control.ControlType.TwoAxisPuppet:
|
||||
{
|
||||
EnsureParameterCount(2);
|
||||
EnsureLabelCount(4);
|
||||
|
||||
EditorGUILayout.LabelField("Parameters", EditorStyles.boldLabel);
|
||||
EditorGUILayout.Space(2);
|
||||
|
||||
_subParams[0].DoGUI(G("menuitem.param.horizontal"));
|
||||
_subParams[1].DoGUI(G("menuitem.param.vertical"));
|
||||
|
||||
DoFourAxisLabels(false);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case VRCExpressionsMenu.Control.ControlType.FourAxisPuppet:
|
||||
{
|
||||
DoFourAxisLabels(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorGUILayout.EndVertical();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
_obj.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureLabelCount(int i)
|
||||
{
|
||||
if (_labels == null || _labelsRoot.arraySize < i || _labels.Length < i)
|
||||
{
|
||||
_labelsRoot.arraySize = i;
|
||||
_labels = new SerializedProperty[i];
|
||||
for (int j = 0; j < i; j++)
|
||||
{
|
||||
_labels[j] = _labelsRoot.GetArrayElementAtIndex(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CenterLabel(Rect rect, GUIContent content, GUIStyle style)
|
||||
{
|
||||
var size = style.CalcSize(content);
|
||||
var x = rect.x + rect.width / 2 - size.x / 2;
|
||||
var y = rect.y + rect.height / 2 - size.y / 2;
|
||||
GUI.Label(new Rect(x, y, size.x, size.y), content, style);
|
||||
}
|
||||
|
||||
private void DoFourAxisLabels(bool showParams)
|
||||
{
|
||||
float maxWidth = 128 * 3;
|
||||
|
||||
EnsureLabelCount(4);
|
||||
if (showParams) EnsureParameterCount(4);
|
||||
|
||||
float extraHeight = EditorGUIUtility.singleLineHeight * 3;
|
||||
if (showParams) extraHeight += EditorGUIUtility.singleLineHeight;
|
||||
|
||||
EditorGUILayout.LabelField(
|
||||
G(showParams ? "menuitem.label.control_labels_and_params" : "menuitem.label.control_labels"),
|
||||
EditorStyles.boldLabel);
|
||||
|
||||
var square = GUILayoutUtility.GetAspectRect(1, GUILayout.MaxWidth(maxWidth));
|
||||
var extraSpace = GUILayoutUtility.GetRect(0, 0, extraHeight,
|
||||
extraHeight, GUILayout.ExpandWidth(true));
|
||||
|
||||
var rect = square;
|
||||
rect.height += extraSpace.height;
|
||||
|
||||
float extraWidth = Math.Max(0, extraSpace.width - rect.width);
|
||||
rect.x += extraWidth / 2;
|
||||
|
||||
var blockHeight = rect.height / 3;
|
||||
var blockWidth = rect.width / 3;
|
||||
|
||||
var up = rect;
|
||||
up.yMax -= blockHeight * 2;
|
||||
up.xMin += blockWidth;
|
||||
up.xMax -= blockWidth;
|
||||
|
||||
var down = rect;
|
||||
down.yMin += blockHeight * 2;
|
||||
down.xMin += blockWidth;
|
||||
down.xMax -= blockWidth;
|
||||
|
||||
var left = rect;
|
||||
left.yMin += blockHeight;
|
||||
left.yMax -= blockHeight;
|
||||
left.xMax -= blockWidth * 2;
|
||||
|
||||
var right = rect;
|
||||
right.yMin += blockHeight;
|
||||
right.yMax -= blockHeight;
|
||||
right.xMin += blockWidth * 2;
|
||||
|
||||
var center = rect;
|
||||
center.yMin += blockHeight;
|
||||
center.yMax -= blockHeight;
|
||||
center.xMin += blockWidth;
|
||||
center.xMax -= blockWidth;
|
||||
|
||||
SingleLabel(0, up);
|
||||
SingleLabel(1, right);
|
||||
SingleLabel(2, down);
|
||||
SingleLabel(3, left);
|
||||
|
||||
var rect_param_l = center;
|
||||
rect_param_l.yMin = rect_param_l.yMax - EditorGUIUtility.singleLineHeight;
|
||||
var rect_name_l = rect_param_l;
|
||||
if (showParams) rect_name_l.y -= rect_param_l.height;
|
||||
|
||||
if (showParams) CenterLabel(rect_param_l, G("menuitem.prop.parameter"), EditorStyles.label);
|
||||
CenterLabel(rect_name_l, G("menuitem.prop.label"), EditorStyles.label);
|
||||
|
||||
void SingleLabel(int index, Rect block)
|
||||
{
|
||||
var prop_name = _labels[index].FindPropertyRelative(nameof(VRCExpressionsMenu.Control.Label.name));
|
||||
var prop_icon = _labels[index].FindPropertyRelative(nameof(VRCExpressionsMenu.Control.Label.icon));
|
||||
|
||||
var rect_param = block;
|
||||
rect_param.yMin = rect_param.yMax - EditorGUIUtility.singleLineHeight;
|
||||
|
||||
var rect_name = rect_param;
|
||||
if (showParams) rect_name.y -= rect_param.height;
|
||||
|
||||
var rect_icon = block;
|
||||
rect_icon.yMax = rect_name.yMin;
|
||||
|
||||
EditorGUI.PropertyField(rect_name, prop_name, GUIContent.none);
|
||||
if (showParams)
|
||||
{
|
||||
_subParams[index].DoGUI(rect_param, GUIContent.none);
|
||||
}
|
||||
|
||||
var tex = prop_icon.objectReferenceValue as Texture;
|
||||
GUIContent icon_content;
|
||||
|
||||
if (prop_icon.hasMultipleDifferentValues)
|
||||
{
|
||||
icon_content = G("menuitem.misc.multiple");
|
||||
}
|
||||
else
|
||||
{
|
||||
icon_content = tex != null ? new GUIContent(tex) : G("menuitem.misc.no_icon");
|
||||
}
|
||||
|
||||
int objectId = GUIUtility.GetControlID(
|
||||
((int) IdGenerator.GetId(this, out bool _) << 2) | index,
|
||||
FocusType.Passive,
|
||||
block
|
||||
);
|
||||
|
||||
if (GUI.Button(rect_icon, icon_content))
|
||||
{
|
||||
texPicker = index;
|
||||
|
||||
EditorGUIUtility.ShowObjectPicker<Texture2D>(
|
||||
prop_icon.hasMultipleDifferentValues ? null : prop_icon.objectReferenceValue, false,
|
||||
"t:texture2d", objectId);
|
||||
}
|
||||
|
||||
if (texPicker == index)
|
||||
{
|
||||
if (Event.current.commandName == "ObjectSelectorUpdated" &&
|
||||
EditorGUIUtility.GetObjectPickerControlID() == objectId)
|
||||
{
|
||||
prop_icon.objectReferenceValue = EditorGUIUtility.GetObjectPickerObject() as Texture;
|
||||
_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureParameterCount(int i)
|
||||
{
|
||||
if (_subParams == null || _subParamsRoot.arraySize < i || _subParams.Length < i)
|
||||
{
|
||||
_subParamsRoot.arraySize = i;
|
||||
_subParams = new ParameterGUI[i];
|
||||
for (int j = 0; j < i; j++)
|
||||
{
|
||||
var prop = _subParamsRoot.GetArrayElementAtIndex(j)
|
||||
.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.Parameter.name));
|
||||
_subParams[j] = new ParameterGUI(_parameterReference, prop, _redraw);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f70714a2a244442e9eabd3fce5a190e6
|
||||
timeCreated: 1677149863
|
@ -0,0 +1,247 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal class MenuPreviewGUI
|
||||
{
|
||||
private const float INDENT_PER_LEVEL = 2;
|
||||
private Action _redraw;
|
||||
private float _indentLevel = 0;
|
||||
private readonly Dictionary<object, Action> _guiNodes = new Dictionary<object, Action>();
|
||||
|
||||
public MenuPreviewGUI(Action redraw)
|
||||
{
|
||||
_redraw = redraw;
|
||||
}
|
||||
|
||||
public void DoGUI(MenuSource root)
|
||||
{
|
||||
_indentLevel = 0;
|
||||
new VisitorContext(this).PushNode(root);
|
||||
}
|
||||
|
||||
public void DoGUI(ModularAvatarMenuInstaller root)
|
||||
{
|
||||
_indentLevel = 0;
|
||||
new VisitorContext(this).PushMenuInstaller(root);
|
||||
}
|
||||
|
||||
public void DoGUI(VRCExpressionsMenu menu, GameObject parameterReference = null)
|
||||
{
|
||||
_indentLevel = 0;
|
||||
new VisitorContext(this).PushNode(menu);
|
||||
}
|
||||
|
||||
private void PushGuiNode(object key, Func<Action> guiBuilder)
|
||||
{
|
||||
if (!_guiNodes.TryGetValue(key, out var gui))
|
||||
{
|
||||
gui = guiBuilder();
|
||||
_guiNodes.Add(key, gui);
|
||||
}
|
||||
|
||||
gui();
|
||||
}
|
||||
|
||||
private class Header
|
||||
{
|
||||
private MenuPreviewGUI _gui;
|
||||
private UnityEngine.Object _headerObj;
|
||||
private SerializedProperty _disableProp;
|
||||
|
||||
public Header(MenuPreviewGUI gui, UnityEngine.Object headerObj, SerializedProperty disableProp = null)
|
||||
{
|
||||
_gui = gui;
|
||||
_headerObj = headerObj;
|
||||
_disableProp = disableProp;
|
||||
}
|
||||
|
||||
public IDisposable Scope()
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Space(_gui._indentLevel);
|
||||
_gui._indentLevel += INDENT_PER_LEVEL;
|
||||
|
||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||
|
||||
if (_headerObj != null)
|
||||
{
|
||||
GUILayout.BeginHorizontal();
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
EditorGUILayout.ObjectField(new GUIContent(), _headerObj, _headerObj.GetType(),
|
||||
true,
|
||||
GUILayout.ExpandWidth(true));
|
||||
}
|
||||
|
||||
if (_disableProp != null)
|
||||
{
|
||||
_disableProp.serializedObject.Update();
|
||||
GUILayout.Space(20);
|
||||
GUILayout.Label("Enabled", GUILayout.Width(50));
|
||||
EditorGUILayout.PropertyField(_disableProp, GUIContent.none,
|
||||
GUILayout.Width(EditorGUIUtility.singleLineHeight));
|
||||
_disableProp.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
return new ScopeSentinel(_gui);
|
||||
}
|
||||
|
||||
private class ScopeSentinel : IDisposable
|
||||
{
|
||||
private readonly MenuPreviewGUI _gui;
|
||||
|
||||
public ScopeSentinel(MenuPreviewGUI gui)
|
||||
{
|
||||
_gui = gui;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GUILayout.EndVertical();
|
||||
_gui._indentLevel -= INDENT_PER_LEVEL;
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class VisitorContext : NodeContext
|
||||
{
|
||||
private readonly HashSet<object> _visited = new HashSet<object>();
|
||||
private readonly MenuPreviewGUI _gui;
|
||||
|
||||
public VisitorContext(MenuPreviewGUI gui)
|
||||
{
|
||||
_gui = gui;
|
||||
}
|
||||
|
||||
public void PushMenu(VRCExpressionsMenu expMenu, GameObject parameterReference = null)
|
||||
{
|
||||
_gui.PushGuiNode((expMenu, parameterReference), () =>
|
||||
{
|
||||
var header = new Header(_gui, expMenu);
|
||||
var obj = new SerializedObject(expMenu);
|
||||
var controls = obj.FindProperty(nameof(expMenu.controls));
|
||||
var subGui = new List<MenuItemCoreGUI>();
|
||||
for (int i = 0; i < controls.arraySize; i++)
|
||||
{
|
||||
subGui.Add(new MenuItemCoreGUI(parameterReference, controls.GetArrayElementAtIndex(i),
|
||||
_gui._redraw));
|
||||
}
|
||||
|
||||
return () =>
|
||||
{
|
||||
using (header.Scope())
|
||||
{
|
||||
foreach (var gui in subGui)
|
||||
{
|
||||
using (new Header(_gui, null).Scope())
|
||||
{
|
||||
gui.DoGUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public void PushNode(VRCExpressionsMenu expMenu)
|
||||
{
|
||||
PushMenu(expMenu, null);
|
||||
}
|
||||
|
||||
public void PushNode(MenuSource source)
|
||||
{
|
||||
if (source is ModularAvatarMenuItem item)
|
||||
{
|
||||
_gui.PushGuiNode(item, () =>
|
||||
{
|
||||
var header = new Header(_gui, item,
|
||||
new SerializedObject(item.gameObject).FindProperty("m_IsActive"));
|
||||
var gui = new MenuItemCoreGUI(new SerializedObject(item), _gui._redraw);
|
||||
return () =>
|
||||
{
|
||||
using (header.Scope())
|
||||
{
|
||||
gui.DoGUI();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
using (new Header(_gui, source as UnityEngine.Object).Scope())
|
||||
{
|
||||
if (_visited.Contains(source)) return;
|
||||
_visited.Add(source);
|
||||
|
||||
source.Visit(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void PushNode(ModularAvatarMenuInstaller installer)
|
||||
{
|
||||
using (new Header(_gui, installer).Scope())
|
||||
{
|
||||
PushMenuInstaller(installer);
|
||||
}
|
||||
}
|
||||
|
||||
internal void PushMenuInstaller(ModularAvatarMenuInstaller installer)
|
||||
{
|
||||
var source = installer.GetComponent<MenuSource>();
|
||||
if (source != null)
|
||||
{
|
||||
PushNode(source);
|
||||
}
|
||||
else if (installer.menuToAppend != null)
|
||||
{
|
||||
PushMenu(installer.menuToAppend, installer.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
public void PushControl(VRCExpressionsMenu.Control control)
|
||||
{
|
||||
// Construct a read-only GUI, as we can't build a serialized property reference for this control object
|
||||
_gui.PushGuiNode(control, () =>
|
||||
{
|
||||
var container = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
||||
container.controls = new List<VRCExpressionsMenu.Control> {control};
|
||||
var prop = new SerializedObject(container).FindProperty("controls").GetArrayElementAtIndex(0);
|
||||
var gui = new MenuItemCoreGUI(null, prop, _gui._redraw);
|
||||
return () =>
|
||||
{
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
gui.DoGUI();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public void PushControl(VirtualControl control)
|
||||
{
|
||||
PushControl((VRCExpressionsMenu.Control) control);
|
||||
}
|
||||
|
||||
public VirtualMenuNode NodeFor(VRCExpressionsMenu menu)
|
||||
{
|
||||
return new VirtualMenuNode(menu);
|
||||
}
|
||||
|
||||
public VirtualMenuNode NodeFor(MenuSource menu)
|
||||
{
|
||||
return new VirtualMenuNode(menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f61c45884f584638a85253fa309f247f
|
||||
timeCreated: 1677302074
|
@ -0,0 +1,278 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using Object = UnityEngine.Object;
|
||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal class ParameterGUI
|
||||
{
|
||||
private GameObject _parameterReference;
|
||||
private readonly SerializedProperty _property;
|
||||
private readonly Action _redraw;
|
||||
private Rect fieldRect;
|
||||
|
||||
internal ParameterGUI(GameObject parameterReference, SerializedProperty property, Action redraw)
|
||||
{
|
||||
_parameterReference = parameterReference;
|
||||
_property = property;
|
||||
_redraw = redraw;
|
||||
}
|
||||
|
||||
public void DoGUI(GUIContent label = null)
|
||||
{
|
||||
DoGUI(EditorGUILayout.GetControlRect(
|
||||
true,
|
||||
EditorGUIUtility.singleLineHeight
|
||||
), label);
|
||||
}
|
||||
|
||||
public void DoGUI(Rect rect, GUIContent label = null)
|
||||
{
|
||||
if (label == null) label = G("menuitem.prop.parameter");
|
||||
|
||||
if (_parameterReference != null) GUILayout.Space(-2);
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
rect.width -= EditorGUIUtility.singleLineHeight;
|
||||
|
||||
EditorGUI.PropertyField(rect, _property, label);
|
||||
Rect propField = new Rect();
|
||||
|
||||
if (Event.current.type == EventType.Repaint)
|
||||
{
|
||||
propField = rect;
|
||||
}
|
||||
|
||||
Rect buttonRect = rect;
|
||||
buttonRect.xMin = rect.xMax;
|
||||
buttonRect.width = EditorGUIUtility.singleLineHeight;
|
||||
|
||||
GUIStyle style = "IN DropDown";
|
||||
|
||||
buttonRect.xMin += (buttonRect.width - style.fixedWidth) / 2;
|
||||
|
||||
if (_parameterReference != null &&
|
||||
EditorGUI.DropdownButton(buttonRect, new GUIContent(), FocusType.Keyboard, style))
|
||||
{
|
||||
PopupWindow.Show(fieldRect,
|
||||
new ParameterWindow(_parameterReference, _property, fieldRect.width, _redraw));
|
||||
}
|
||||
|
||||
if (Event.current.type == EventType.Repaint)
|
||||
{
|
||||
float labelWidth = label == GUIContent.none ? 0 : EditorGUIUtility.labelWidth;
|
||||
|
||||
fieldRect = propField;
|
||||
fieldRect.x += labelWidth + 2;
|
||||
fieldRect.width = buttonRect.xMax - propField.x - labelWidth;
|
||||
fieldRect.height = 0;
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
if (_parameterReference != null) GUILayout.Space(2);
|
||||
}
|
||||
|
||||
private class ParameterWindow : PopupWindowContent
|
||||
{
|
||||
private readonly GameObject _target;
|
||||
private readonly SerializedProperty _prop;
|
||||
private readonly float _width;
|
||||
private readonly Action _redraw;
|
||||
|
||||
private SearchField _searchField;
|
||||
private ParameterTree _tree;
|
||||
private string _searchString;
|
||||
|
||||
public ParameterWindow(GameObject target, SerializedProperty prop, float width, Action redraw)
|
||||
{
|
||||
_target = target;
|
||||
_prop = prop;
|
||||
_width = width;
|
||||
_redraw = redraw;
|
||||
}
|
||||
|
||||
public override void OnGUI(Rect rect)
|
||||
{
|
||||
var sfRect = rect;
|
||||
sfRect.height = EditorGUIUtility.singleLineHeight;
|
||||
rect.y += EditorGUIUtility.singleLineHeight;
|
||||
rect.height -= EditorGUIUtility.singleLineHeight;
|
||||
|
||||
if (_searchField == null)
|
||||
{
|
||||
_searchField = new SearchField();
|
||||
}
|
||||
|
||||
_searchString = _searchField.OnGUI(sfRect, _searchString);
|
||||
|
||||
if (_tree == null)
|
||||
{
|
||||
_tree = new ParameterTree(new TreeViewState(), _target);
|
||||
_tree.OnSelect = (s) =>
|
||||
{
|
||||
_prop.stringValue = s;
|
||||
_prop.serializedObject.ApplyModifiedProperties();
|
||||
_redraw();
|
||||
};
|
||||
|
||||
_tree.OnCommit = (s) =>
|
||||
{
|
||||
_prop.stringValue = s;
|
||||
_prop.serializedObject.ApplyModifiedProperties();
|
||||
editorWindow.Close();
|
||||
_redraw();
|
||||
};
|
||||
_tree.Reload();
|
||||
}
|
||||
|
||||
_tree.searchString = _searchString;
|
||||
_tree.OnGUI(rect);
|
||||
}
|
||||
|
||||
public override Vector2 GetWindowSize()
|
||||
{
|
||||
return new Vector2(Math.Max(256, _width), 150);
|
||||
}
|
||||
}
|
||||
|
||||
private class ParameterTree : TreeView
|
||||
{
|
||||
private List<string> _items;
|
||||
public Action<string> OnSelect, OnCommit;
|
||||
|
||||
private GameObject _obj;
|
||||
|
||||
private class SourceItem : TreeViewItem
|
||||
{
|
||||
public GameObject source;
|
||||
}
|
||||
|
||||
private class ParamItem : TreeViewItem
|
||||
{
|
||||
public GameObject source;
|
||||
}
|
||||
|
||||
public ParameterTree(TreeViewState state, GameObject obj) : base(state)
|
||||
{
|
||||
_obj = obj;
|
||||
}
|
||||
|
||||
protected override void SelectionChanged(IList<int> selectedIds)
|
||||
{
|
||||
var item = _items[selectedIds[0]];
|
||||
if (item != null) OnSelect(item);
|
||||
}
|
||||
|
||||
protected override void DoubleClickedItem(int id)
|
||||
{
|
||||
var item = _items[id];
|
||||
if (item != null) OnCommit(item);
|
||||
}
|
||||
|
||||
protected override void RowGUI(RowGUIArgs args)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(searchString) && args.item is ParamItem offer)
|
||||
{
|
||||
var rect = args.rowRect;
|
||||
|
||||
var objName = offer.source.name + " / ";
|
||||
var content = new GUIContent(objName);
|
||||
|
||||
var width = EditorStyles.label.CalcSize(content).x;
|
||||
var color = GUI.color;
|
||||
|
||||
var grey = color;
|
||||
grey.a *= 0.7f;
|
||||
GUI.color = grey;
|
||||
|
||||
EditorGUI.LabelField(rect, content);
|
||||
|
||||
GUI.color = color;
|
||||
|
||||
rect.x += width;
|
||||
rect.width -= width;
|
||||
|
||||
if (rect.width >= 0)
|
||||
{
|
||||
EditorGUI.LabelField(rect, offer.displayName);
|
||||
}
|
||||
}
|
||||
else if (args.item is SourceItem source)
|
||||
{
|
||||
var rect = args.rowRect;
|
||||
rect.xMin += this.GetContentIndent(args.item) + this.extraSpaceBeforeIconAndLabel;
|
||||
EditorGUI.LabelField(rect, source.source.name);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.RowGUI(args);
|
||||
}
|
||||
}
|
||||
|
||||
protected override TreeViewItem BuildRoot()
|
||||
{
|
||||
List<TreeViewItem> treeItems = new List<TreeViewItem>();
|
||||
_items = new List<string>();
|
||||
|
||||
_items.Add("");
|
||||
var root = new TreeViewItem {id = 0, depth = -1, displayName = "Root"};
|
||||
|
||||
GameObject priorNode = null;
|
||||
|
||||
foreach ((GameObject node, string param) in FindParameters())
|
||||
{
|
||||
if (node != priorNode)
|
||||
{
|
||||
_items.Add(null);
|
||||
treeItems.Add(new SourceItem()
|
||||
{id = _items.Count - 1, depth = 0, displayName = "", source = node});
|
||||
priorNode = node;
|
||||
}
|
||||
|
||||
_items.Add(param);
|
||||
treeItems.Add(new ParamItem {id = _items.Count - 1, depth = 1, displayName = param, source = node});
|
||||
}
|
||||
|
||||
SetupParentsAndChildrenFromDepths(root, treeItems);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private IEnumerable<(GameObject, string)> FindParameters()
|
||||
{
|
||||
HashSet<string> emitted = new HashSet<string>();
|
||||
GameObject node = _obj;
|
||||
while (node != null && node.GetComponent<VRCAvatarDescriptor>() == null)
|
||||
{
|
||||
var paramComp = node.GetComponent<ModularAvatarParameters>();
|
||||
if (paramComp != null)
|
||||
{
|
||||
foreach (var param in paramComp.parameters)
|
||||
{
|
||||
if (!param.isPrefix)
|
||||
{
|
||||
if (emitted.Add(param.nameOrPrefix)) yield return (node, param.nameOrPrefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node = node.transform.parent?.gameObject;
|
||||
}
|
||||
|
||||
var desc = node?.GetComponent<VRCAvatarDescriptor>();
|
||||
if (desc != null)
|
||||
{
|
||||
foreach (var param in desc.expressionParameters.parameters)
|
||||
{
|
||||
if (emitted.Add(param.name)) yield return (node, param.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69bb3e09018e430581cabad176e3068e
|
||||
timeCreated: 1677148788
|
@ -1,304 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||
using static nadena.dev.modular_avatar.core.editor.Util;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
[CustomEditor(typeof(ModularAvatarMenuInstaller))]
|
||||
[CanEditMultipleObjects]
|
||||
internal class MenuInstallerEditor : MAEditorBase
|
||||
{
|
||||
private ModularAvatarMenuInstaller _installer;
|
||||
private Editor _innerMenuEditor;
|
||||
private VRCExpressionsMenu _menuToAppend;
|
||||
|
||||
private bool _menuFoldout;
|
||||
private bool _devFoldout;
|
||||
|
||||
private HashSet<VRCExpressionsMenu> _avatarMenus;
|
||||
|
||||
private Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>> _menuInstallersMap;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_installer = (ModularAvatarMenuInstaller) target;
|
||||
|
||||
FindMenus();
|
||||
FindMenuInstallers();
|
||||
}
|
||||
|
||||
private void SetupMenuEditor()
|
||||
{
|
||||
if (targets.Length != 1)
|
||||
{
|
||||
_innerMenuEditor = null;
|
||||
_menuToAppend = null;
|
||||
}
|
||||
else if (_installer.menuToAppend != _menuToAppend)
|
||||
{
|
||||
if (_installer.menuToAppend == null) _innerMenuEditor = null;
|
||||
else
|
||||
{
|
||||
_innerMenuEditor = CreateEditor(_installer.menuToAppend);
|
||||
}
|
||||
|
||||
_menuToAppend = _installer.menuToAppend;
|
||||
}
|
||||
}
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
SetupMenuEditor();
|
||||
|
||||
var installTo = serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.installTargetMenu));
|
||||
|
||||
var isEnabled = targets.Length != 1 || ((ModularAvatarMenuInstaller) target).enabled;
|
||||
|
||||
VRCAvatarDescriptor commonAvatar = FindCommonAvatar();
|
||||
|
||||
if (!installTo.hasMultipleDifferentValues)
|
||||
{
|
||||
if (installTo.objectReferenceValue == null)
|
||||
{
|
||||
if (isEnabled)
|
||||
{
|
||||
EditorGUILayout.HelpBox(S("menuinstall.help.hint_set_menu"), MessageType.Info);
|
||||
}
|
||||
}
|
||||
else if (!IsMenuReachable(RuntimeUtil.FindAvatarInParents(((Component) target).transform),
|
||||
(VRCExpressionsMenu) installTo.objectReferenceValue))
|
||||
{
|
||||
EditorGUILayout.HelpBox(S("menuinstall.help.hint_bad_menu"), MessageType.Error);
|
||||
}
|
||||
}
|
||||
|
||||
if (installTo.hasMultipleDifferentValues || commonAvatar == null)
|
||||
{
|
||||
EditorGUILayout.PropertyField(installTo, G("menuinstall.installto"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var displayValue = installTo.objectReferenceValue;
|
||||
if (displayValue == null) displayValue = commonAvatar.expressionsMenu;
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var newValue = EditorGUILayout.ObjectField(G("menuinstall.installto"), displayValue,
|
||||
typeof(VRCExpressionsMenu), false);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
installTo.objectReferenceValue = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
var avatar = RuntimeUtil.FindAvatarInParents(_installer.transform);
|
||||
if (avatar != null && GUILayout.Button(G("menuinstall.selectmenu")))
|
||||
{
|
||||
AvMenuTreeViewWindow.Show(avatar, _installer, menu =>
|
||||
{
|
||||
installTo.objectReferenceValue = menu;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
});
|
||||
}
|
||||
|
||||
if (targets.Length == 1)
|
||||
{
|
||||
_menuFoldout = EditorGUILayout.Foldout(_menuFoldout, G("menuinstall.showcontents"));
|
||||
if (_menuFoldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
using (var disabled = new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
if (_innerMenuEditor != null) _innerMenuEditor.OnInspectorGUI();
|
||||
else EditorGUILayout.HelpBox(S("menuinstall.showcontents.notselected"), MessageType.Info);
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
|
||||
_devFoldout = EditorGUILayout.Foldout(_devFoldout, G("menuinstall.devoptions"));
|
||||
if (_devFoldout)
|
||||
{
|
||||
SerializedProperty menuToAppendProperty = serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.menuToAppend));
|
||||
switch (ValidateExpressionMenuIcon((VRCExpressionsMenu)menuToAppendProperty.objectReferenceValue))
|
||||
{
|
||||
case ValidateExpressionMenuIconResult.Success:
|
||||
break;
|
||||
case ValidateExpressionMenuIconResult.TooLarge:
|
||||
EditorGUILayout.HelpBox(S("menuinstall.menu_icon_too_large"), MessageType.Error);
|
||||
break;
|
||||
case ValidateExpressionMenuIconResult.Uncompressed:
|
||||
EditorGUILayout.HelpBox(S("menuinstall.menu_icon_uncompressed"), MessageType.Error);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.PropertyField(
|
||||
menuToAppendProperty, new GUIContent(G("menuinstall.srcmenu")));
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
Localization.ShowLanguageUI();
|
||||
}
|
||||
|
||||
private VRCAvatarDescriptor FindCommonAvatar()
|
||||
{
|
||||
VRCAvatarDescriptor commonAvatar = null;
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var component = (ModularAvatarMenuInstaller) target;
|
||||
var avatar = RuntimeUtil.FindAvatarInParents(component.transform);
|
||||
if (avatar == null) return null;
|
||||
|
||||
if (commonAvatar == null)
|
||||
{
|
||||
commonAvatar = avatar;
|
||||
}
|
||||
else if (commonAvatar != avatar)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return commonAvatar;
|
||||
}
|
||||
|
||||
private void FindMenus()
|
||||
{
|
||||
if (targets.Length > 1)
|
||||
{
|
||||
_avatarMenus = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_avatarMenus = new HashSet<VRCExpressionsMenu>();
|
||||
var queue = new Queue<VRCExpressionsMenu>();
|
||||
var avatar = RuntimeUtil.FindAvatarInParents(((Component) target).transform);
|
||||
if (avatar == null || avatar.expressionsMenu == null) return;
|
||||
queue.Enqueue(avatar.expressionsMenu);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var menu = queue.Dequeue();
|
||||
if (_avatarMenus.Contains(menu)) continue;
|
||||
|
||||
_avatarMenus.Add(menu);
|
||||
foreach (var subMenu in menu.controls)
|
||||
{
|
||||
if (subMenu.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
|
||||
{
|
||||
queue.Enqueue(subMenu.subMenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FindMenuInstallers()
|
||||
{
|
||||
if (targets.Length > 1)
|
||||
{
|
||||
_menuInstallersMap = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_menuInstallersMap = new Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>>();
|
||||
var avatar = RuntimeUtil.FindAvatarInParents(((Component)target).transform);
|
||||
if (avatar == null) return;
|
||||
var menuInstallers = avatar.GetComponentsInChildren<ModularAvatarMenuInstaller>(true)
|
||||
.Where(menuInstaller => menuInstaller.enabled && menuInstaller.menuToAppend != null);
|
||||
foreach (ModularAvatarMenuInstaller menuInstaller in menuInstallers)
|
||||
{
|
||||
if (menuInstaller == target) continue;
|
||||
var visitedMenus = new HashSet<VRCExpressionsMenu>();
|
||||
var queue = new Queue<VRCExpressionsMenu>();
|
||||
queue.Enqueue(menuInstaller.menuToAppend);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
VRCExpressionsMenu parent = queue.Dequeue();
|
||||
var controls = parent.controls.Where(control => control.type == VRCExpressionsMenu.Control.ControlType.SubMenu && control.subMenu != null);
|
||||
foreach (VRCExpressionsMenu.Control control in controls)
|
||||
{
|
||||
// Do not filter in LINQ to avoid closure allocation
|
||||
if (visitedMenus.Contains(control.subMenu)) continue;
|
||||
if (!_menuInstallersMap.TryGetValue(control.subMenu, out List<ModularAvatarMenuInstaller> fromInstallers))
|
||||
{
|
||||
fromInstallers = new List<ModularAvatarMenuInstaller>();
|
||||
_menuInstallersMap[control.subMenu] = fromInstallers;
|
||||
}
|
||||
|
||||
fromInstallers.Add(menuInstaller);
|
||||
visitedMenus.Add(control.subMenu);
|
||||
queue.Enqueue(control.subMenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsMenuReachable(VRCAvatarDescriptor avatar, VRCExpressionsMenu menu, HashSet<ModularAvatarMenuInstaller> visitedInstaller = null)
|
||||
{
|
||||
if (_avatarMenus == null || _avatarMenus.Contains(menu)) return true;
|
||||
|
||||
if (_menuInstallersMap == null) return true;
|
||||
if (visitedInstaller == null) visitedInstaller = new HashSet<ModularAvatarMenuInstaller> { (ModularAvatarMenuInstaller)target };
|
||||
|
||||
if (!_menuInstallersMap.TryGetValue(menu, out List<ModularAvatarMenuInstaller> installers)) return false;
|
||||
foreach (ModularAvatarMenuInstaller installer in installers)
|
||||
{
|
||||
// Root is always reachable if installTargetMenu is null
|
||||
if (installer.installTargetMenu == null) return true;
|
||||
// Even in a circular structure, it may be possible to reach root by another path.
|
||||
if (visitedInstaller.Contains(installer)) continue;
|
||||
visitedInstaller.Add(installer);
|
||||
if (IsMenuReachable(avatar, installer.installTargetMenu, visitedInstaller))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ValidateExpressionMenuIconResult ValidateExpressionMenuIcon(VRCExpressionsMenu menu, HashSet<VRCExpressionsMenu> visitedMenus = null)
|
||||
{
|
||||
if (menu == null) return ValidateExpressionMenuIconResult.Success;
|
||||
if (visitedMenus == null) visitedMenus = new HashSet<VRCExpressionsMenu>();
|
||||
if (visitedMenus.Contains(menu)) return ValidateExpressionMenuIconResult.Success;
|
||||
visitedMenus.Add(menu);
|
||||
|
||||
foreach (VRCExpressionsMenu.Control control in menu.controls) {
|
||||
// Control
|
||||
ValidateExpressionMenuIconResult result = Util.ValidateExpressionMenuIcon(control.icon);
|
||||
if (result != ValidateExpressionMenuIconResult.Success) return result;
|
||||
|
||||
// Labels
|
||||
if (control.labels != null)
|
||||
{
|
||||
foreach (VRCExpressionsMenu.Control.Label label in control.labels)
|
||||
{
|
||||
ValidateExpressionMenuIconResult labelResult = Util.ValidateExpressionMenuIcon(label.icon);
|
||||
if (labelResult != ValidateExpressionMenuIconResult.Success) return labelResult;
|
||||
}
|
||||
}
|
||||
|
||||
// SubMenu
|
||||
if (control.type != VRCExpressionsMenu.Control.ControlType.SubMenu) continue;
|
||||
ValidateExpressionMenuIconResult subMenuResult = ValidateExpressionMenuIcon(control.subMenu, visitedMenus);
|
||||
if (subMenuResult != ValidateExpressionMenuIconResult.Success) return subMenuResult;
|
||||
}
|
||||
|
||||
return ValidateExpressionMenuIconResult.Success;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -78,5 +78,33 @@
|
||||
"validation.menu_installer.no_menu": "No menu to install specified",
|
||||
"validation.merge_animator.no_animator": "No animator to merge specified",
|
||||
"validation.merge_armature.no_target": "No merge target specified",
|
||||
"validation.merge_armature.target_is_child": "Merge target cannot be a child of this object"
|
||||
"validation.merge_armature.target_is_child": "Merge target cannot be a child of this object",
|
||||
"submenu_source.Children": "Children",
|
||||
"submenu_source.MenuAsset": "Expressions Menu Asset",
|
||||
"menuitem.showcontents": "Show menu contents",
|
||||
"menuitem.prop.name": "Name",
|
||||
"menuitem.prop.icon": "Icon",
|
||||
"menuitem.prop.icon.tooltip": "(Optional) The icon to be shown in the expressions menu",
|
||||
"menuitem.prop.type": "Type",
|
||||
"menuitem.prop.type.tooltip": "The type of this item",
|
||||
"menuitem.prop.value": "Value",
|
||||
"menuitem.prop.value.tooltip": "The value to set the parameter to when this control is used",
|
||||
"menuitem.prop.parameter": "Parameter",
|
||||
"menuitem.prop.label": "Label",
|
||||
"menuitem.prop.submenu_asset": "Submenu Asset",
|
||||
"menuitem.prop.submenu_asset.tooltip": "The asset to use as the submenu",
|
||||
"menuitem.prop.submenu_source": "Submenu Source",
|
||||
"menuitem.prop.submenu_source.tooltip": "Where to find the items to put inside this submenu",
|
||||
"menuitem.prop.source_override": "Source object override",
|
||||
"menuitem.prop.source_override.tooltip": "If specified, this object will be used as the source for the contents of the submenu. Otherwise, children of this menu item will be used.",
|
||||
"menuitem.param.rotation": "Parameter: Rotation",
|
||||
"menuitem.param.rotation.tooltip": "The parameter to set based on the rotation of this menu item",
|
||||
"menuitem.param.horizontal": "Parameter: Horizontal",
|
||||
"menuitem.param.horizontal.tooltip": "The parameter to set based on the horizontal position of the thumbstick",
|
||||
"menuitem.param.vertical": "Parameter: Vertical",
|
||||
"menuitem.param.vertical.tooltip": "The parameter to set based on the vertical position of the thumbstick",
|
||||
"menuitem.label.control_labels_and_params": "Control Labels and Parameters",
|
||||
"menuitem.label.control_labels": "Control Labels",
|
||||
"menuitem.misc.multiple": "(multiple)",
|
||||
"menuitem.misc.no_icon": "(no icon)"
|
||||
}
|
@ -75,5 +75,33 @@
|
||||
"validation.bone_proxy.no_target": "ターゲットオブジェクトが未設定、もしくは存在しません。",
|
||||
"validation.menu_installer.no_menu": "インストールするメニューがありません。",
|
||||
"validation.merge_animator.no_animator": "Animator Controllerがありません。",
|
||||
"validation.merge_armature.no_target": "ターゲットオブジェクトが未設定、もしくは存在しません。"
|
||||
"validation.merge_armature.no_target": "ターゲットオブジェクトが未設定、もしくは存在しません。",
|
||||
"submenu_source.Children": "子オブジェクトから生成",
|
||||
"submenu_source.MenuAsset": "Expressions Menu アセットを指定",
|
||||
"menuitem.showcontents": "メニュー内容を表示",
|
||||
"menuitem.prop.name": "表示名",
|
||||
"menuitem.prop.icon": "アイコン",
|
||||
"menuitem.prop.icon.tooltip": "(任意) メニューに表示するべきアイコン",
|
||||
"menuitem.prop.type": "タイプ",
|
||||
"menuitem.prop.type.tooltip": "この項目の種別",
|
||||
"menuitem.prop.value": "パラメーター値",
|
||||
"menuitem.prop.value.tooltip": "この項目が操作されたとき、パラメーターが設定される値",
|
||||
"menuitem.prop.parameter": "パラメーター名",
|
||||
"menuitem.prop.label": "表示名",
|
||||
"menuitem.prop.submenu_asset": "サブメニューアセット",
|
||||
"menuitem.prop.submenu_asset.tooltip": "サブメニューとして引用するアセット",
|
||||
"menuitem.prop.submenu_source": "サブメニュー引用元",
|
||||
"menuitem.prop.submenu_source.tooltip": "このサブメニューの内容をどこから引用するべきかを指定",
|
||||
"menuitem.prop.source_override": "引用元オブジェクト",
|
||||
"menuitem.prop.source_override.tooltip": "指定した場合は、指定したオブジェクトの子をメニューの内容として指定します。指定されてない場合はこのオブジェクト直下の子を使用します。",
|
||||
"menuitem.param.rotation": "回転パラメーター名",
|
||||
"menuitem.param.rotation.tooltip": "このメニューアイテムの回転に連動するべきパラメーター",
|
||||
"menuitem.param.horizontal": "横パラメーター名",
|
||||
"menuitem.param.horizontal.tooltip": "横操作に連動するパラメーター名",
|
||||
"menuitem.param.vertical": "立てパラメーター名",
|
||||
"menuitem.param.vertical.tooltip": "上下操作に連動するパラメーター名",
|
||||
"menuitem.label.control_labels_and_params": "表示名・パラメーター",
|
||||
"menuitem.label.control_labels": "表示名",
|
||||
"menuitem.misc.multiple": "(複数設定)",
|
||||
"menuitem.misc.no_icon": "(アイコン無し)"
|
||||
}
|
||||
|
4
Packages/nadena.dev.modular-avatar/Editor/Menu.meta
Normal file
4
Packages/nadena.dev.modular-avatar/Editor/Menu.meta
Normal file
@ -0,0 +1,4 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 435b9162d1fd4050b9ced045ab20af27
|
||||
timeCreated: 1676977199
|
||||
folderAsset: yes
|
@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal static class ClonedMenuMappings
|
||||
{
|
||||
/// <summary>
|
||||
/// Map to link the cloned menu from the clone source.
|
||||
/// If one menu is specified for multiple installers, they are replicated separately, so there is a one-to-many relationship.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<VRCExpressionsMenu, ImmutableList<VRCExpressionsMenu>> ClonedMappings =
|
||||
new Dictionary<VRCExpressionsMenu, ImmutableList<VRCExpressionsMenu>>();
|
||||
|
||||
/// <summary>
|
||||
/// Map to link the clone source from the cloned menu.
|
||||
/// Map is the opposite of ClonedMappings.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> OriginalMapping =
|
||||
new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
ClonedMappings.Clear();
|
||||
OriginalMapping.Clear();
|
||||
}
|
||||
|
||||
public static void Add(VRCExpressionsMenu original, VRCExpressionsMenu clonedMenu)
|
||||
{
|
||||
if (!ClonedMappings.TryGetValue(original, out ImmutableList<VRCExpressionsMenu> clonedMenus))
|
||||
{
|
||||
clonedMenus = ImmutableList<VRCExpressionsMenu>.Empty;
|
||||
}
|
||||
|
||||
ClonedMappings[original] = clonedMenus.Add(clonedMenu);
|
||||
OriginalMapping[clonedMenu] = original;
|
||||
}
|
||||
|
||||
public static bool TryGetClonedMenus(VRCExpressionsMenu original,
|
||||
out ImmutableList<VRCExpressionsMenu> clonedMenus)
|
||||
{
|
||||
return ClonedMappings.TryGetValue(original, out clonedMenus);
|
||||
}
|
||||
|
||||
public static VRCExpressionsMenu GetOriginal(VRCExpressionsMenu cloned)
|
||||
{
|
||||
return OriginalMapping.TryGetValue(cloned, out VRCExpressionsMenu original) ? original : null;
|
||||
}
|
||||
}
|
||||
}
|
133
Packages/nadena.dev.modular-avatar/Editor/Menu/MenuExtractor.cs
Normal file
133
Packages/nadena.dev.modular-avatar/Editor/Menu/MenuExtractor.cs
Normal file
@ -0,0 +1,133 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal class MenuExtractor
|
||||
{
|
||||
private const int PRIORITY = 49;
|
||||
|
||||
[MenuItem("GameObject/[Modular Avatar] Extract menu", false, PRIORITY)]
|
||||
static void ExtractMenu(MenuCommand menuCommand)
|
||||
{
|
||||
if (!(menuCommand.context is GameObject gameObj)) return;
|
||||
var avatar = gameObj.GetComponent<VRCAvatarDescriptor>();
|
||||
if (avatar == null || avatar.expressionsMenu == null) return;
|
||||
|
||||
var parent = ExtractSingleLayerMenu(avatar.expressionsMenu, gameObj, "Avatar Menu");
|
||||
parent.AddComponent<ModularAvatarMenuInstaller>();
|
||||
parent.AddComponent<ModularAvatarMenuGroup>();
|
||||
|
||||
// The VRCSDK requires that an expressions menu asset be provided if any parameters are defined.
|
||||
// We can't just remove the asset, so we'll replace it with a dummy asset. However, to avoid users
|
||||
// accidentally overwriting files in Packages, we'll place this dummy asset next to where the original
|
||||
// asset was (or in the Assets root, if the original asset was in Packages).
|
||||
Undo.RecordObject(avatar, "Extract menu");
|
||||
|
||||
var assetPath = AssetDatabase.GetAssetPath(avatar.expressionsMenu);
|
||||
var dummyAssetPathBase = assetPath.Replace(".asset", " placeholder");
|
||||
if (dummyAssetPathBase.StartsWith("Packages" + System.IO.Path.DirectorySeparatorChar))
|
||||
{
|
||||
var filename = System.IO.Path.GetFileName(dummyAssetPathBase);
|
||||
dummyAssetPathBase = System.IO.Path.Combine("Assets", filename);
|
||||
}
|
||||
|
||||
// Check that a similarly-named file doesn't already exist
|
||||
int i = 0;
|
||||
do
|
||||
{
|
||||
var fullPath = dummyAssetPathBase + (i > 0 ? " " + i : "") + ".asset";
|
||||
if (System.IO.File.Exists(fullPath))
|
||||
{
|
||||
var asset = AssetDatabase.LoadAssetAtPath<VRCExpressionsMenu>(fullPath);
|
||||
if (asset != null && asset.controls.Count == 0)
|
||||
{
|
||||
avatar.expressionsMenu = asset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (!System.IO.File.Exists(fullPath))
|
||||
{
|
||||
var dummyAsset = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
||||
AssetDatabase.CreateAsset(dummyAsset, fullPath);
|
||||
avatar.expressionsMenu = dummyAsset;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
i++;
|
||||
} while (true);
|
||||
|
||||
EditorUtility.SetDirty(avatar);
|
||||
PrefabUtility.RecordPrefabInstancePropertyModifications(avatar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts a single expressions menu asset to Menu Item components.
|
||||
/// </summary>
|
||||
/// <param name="menu">The menu to extract</param>
|
||||
/// <param name="parent">The parent object to use</param>
|
||||
/// <param name="containerName">The name of a gameobject to place between the parent and menu item objects,
|
||||
/// or null to skip</param>
|
||||
/// <returns>the direct parent of the generated menu items</returns>
|
||||
internal static GameObject ExtractSingleLayerMenu(
|
||||
VRCExpressionsMenu menu,
|
||||
GameObject parent,
|
||||
string containerName = null)
|
||||
{
|
||||
if (containerName != null)
|
||||
{
|
||||
var container = new GameObject();
|
||||
container.name = containerName;
|
||||
container.transform.SetParent(parent.transform, false);
|
||||
parent = container;
|
||||
Undo.RegisterCreatedObjectUndo(container, "Convert menu");
|
||||
}
|
||||
|
||||
foreach (var control in menu.controls)
|
||||
{
|
||||
var itemObj = new GameObject();
|
||||
itemObj.name = string.IsNullOrEmpty(control.name) ? " " : control.name;
|
||||
Undo.RegisterCreatedObjectUndo(itemObj, "Convert menu");
|
||||
itemObj.transform.SetParent(parent.transform, false);
|
||||
|
||||
var menuItem = itemObj.AddComponent<ModularAvatarMenuItem>();
|
||||
ControlToMenuItem(menuItem, control);
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
internal static void ControlToMenuItem(ModularAvatarMenuItem menuItem, VRCExpressionsMenu.Control control)
|
||||
{
|
||||
menuItem.Control = CloneControl(control);
|
||||
if (menuItem.Control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
|
||||
{
|
||||
menuItem.MenuSource = SubmenuSource.MenuAsset;
|
||||
}
|
||||
}
|
||||
|
||||
internal static VRCExpressionsMenu.Control CloneControl(VRCExpressionsMenu.Control c)
|
||||
{
|
||||
return new VRCExpressionsMenu.Control()
|
||||
{
|
||||
type = c.type,
|
||||
name = c.name,
|
||||
icon = c.icon,
|
||||
parameter = new VRCExpressionsMenu.Control.Parameter() {name = c.parameter?.name},
|
||||
subMenu = c.subMenu,
|
||||
subParameters = c.subParameters?.Select(p =>
|
||||
new VRCExpressionsMenu.Control.Parameter() {name = p?.name})
|
||||
.ToArray(),
|
||||
labels = c.labels.ToArray(),
|
||||
style = c.style,
|
||||
value = c.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de7ea831512b4d9c9e92985ab6fd5f17
|
||||
timeCreated: 1676896326
|
378
Packages/nadena.dev.modular-avatar/Editor/Menu/VirtualMenu.cs
Normal file
378
Packages/nadena.dev.modular-avatar/Editor/Menu/VirtualMenu.cs
Normal file
@ -0,0 +1,378 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.Components;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor.menu
|
||||
{
|
||||
/// <summary>
|
||||
/// Sentinel object to represent the avatar root menu (for avatars which don't have a root menu)
|
||||
/// </summary>
|
||||
internal sealed class RootMenu : MenuSource
|
||||
{
|
||||
public static readonly RootMenu Instance = new RootMenu();
|
||||
|
||||
private RootMenu()
|
||||
{
|
||||
}
|
||||
|
||||
public void Visit(NodeContext context)
|
||||
{
|
||||
// we initialize the root node manually
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
class NodeContextImpl : NodeContext
|
||||
{
|
||||
[CanBeNull]
|
||||
internal delegate VirtualMenuNode NodeForDelegate(object menu);
|
||||
|
||||
private readonly ImmutableDictionary<object, ImmutableList<ModularAvatarMenuInstaller>>
|
||||
_menuToInstallerMap;
|
||||
|
||||
private readonly VirtualMenuNode _node;
|
||||
private readonly NodeForDelegate _nodeFor;
|
||||
private readonly Action<VRCExpressionsMenu> _visitedMenu;
|
||||
private readonly HashSet<object> _visited = new HashSet<object>();
|
||||
|
||||
public NodeContextImpl(
|
||||
VirtualMenuNode node,
|
||||
NodeForDelegate nodeFor,
|
||||
ImmutableDictionary<object, ImmutableList<ModularAvatarMenuInstaller>> menuToInstallerMap,
|
||||
Action<VRCExpressionsMenu> visitedMenu
|
||||
)
|
||||
{
|
||||
_node = node;
|
||||
_nodeFor = nodeFor;
|
||||
_menuToInstallerMap = menuToInstallerMap;
|
||||
_visitedMenu = visitedMenu;
|
||||
}
|
||||
|
||||
public void PushNode(VRCExpressionsMenu expMenu)
|
||||
{
|
||||
if (expMenu == null) return;
|
||||
if (_visited.Contains(expMenu)) return;
|
||||
_visited.Add(expMenu);
|
||||
_visitedMenu(expMenu);
|
||||
|
||||
foreach (var control in expMenu.controls)
|
||||
{
|
||||
PushControl(control);
|
||||
}
|
||||
|
||||
if (_menuToInstallerMap.TryGetValue(expMenu, out var installers))
|
||||
{
|
||||
foreach (var installer in installers)
|
||||
{
|
||||
PushNode(installer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void PushNode(MenuSource source)
|
||||
{
|
||||
if (source == null) return;
|
||||
if (_visited.Contains(source)) return;
|
||||
_visited.Add(source);
|
||||
|
||||
BuildReport.ReportingObject(source as UnityEngine.Object, () => source.Visit(this));
|
||||
}
|
||||
|
||||
public void PushNode(ModularAvatarMenuInstaller installer)
|
||||
{
|
||||
if (installer == null) return;
|
||||
if (_visited.Contains(installer)) return;
|
||||
_visited.Add(installer);
|
||||
|
||||
BuildReport.ReportingObject(installer, () =>
|
||||
{
|
||||
var menuSourceComp = installer.GetComponent<MenuSourceComponent>();
|
||||
if (menuSourceComp != null)
|
||||
{
|
||||
PushNode(menuSourceComp);
|
||||
}
|
||||
else if (installer.menuToAppend != null)
|
||||
{
|
||||
PushNode(installer.menuToAppend);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void PushControl(VRCExpressionsMenu.Control control)
|
||||
{
|
||||
var virtualControl = new VirtualControl(control);
|
||||
|
||||
virtualControl.SubmenuNode = NodeFor(control.subMenu);
|
||||
|
||||
PushControl(virtualControl);
|
||||
}
|
||||
|
||||
public void PushControl(VirtualControl control)
|
||||
{
|
||||
_node.Controls.Add(control);
|
||||
}
|
||||
|
||||
public VirtualMenuNode NodeFor(VRCExpressionsMenu menu)
|
||||
{
|
||||
if (menu == null) return null;
|
||||
return _nodeFor(menu);
|
||||
}
|
||||
|
||||
public VirtualMenuNode NodeFor(MenuSource source)
|
||||
{
|
||||
if (source == null) return null;
|
||||
return _nodeFor(source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The VirtualMenu class tracks a fully realized shadow menu. Notably, this is _not_ converted to unity
|
||||
* ScriptableObjects, making it easier to discard it when we need to update it.
|
||||
*/
|
||||
internal class VirtualMenu
|
||||
{
|
||||
internal readonly object RootMenuKey;
|
||||
|
||||
private static long _cacheSeq = 0;
|
||||
|
||||
internal static void InvalidateCaches()
|
||||
{
|
||||
_cacheSeq++;
|
||||
}
|
||||
|
||||
static VirtualMenu()
|
||||
{
|
||||
RuntimeUtil.OnMenuInvalidate += InvalidateCaches;
|
||||
}
|
||||
|
||||
internal static long CacheSequence => _cacheSeq;
|
||||
|
||||
private readonly long _initialCacheSeq = _cacheSeq;
|
||||
internal bool IsOutdated => _initialCacheSeq != _cacheSeq;
|
||||
|
||||
/// <summary>
|
||||
/// Indexes which menu installers are contributing to which VRCExpressionMenu assets.
|
||||
/// </summary>
|
||||
private Dictionary<object, List<ModularAvatarMenuInstaller>> _targetMenuToInstaller
|
||||
= new Dictionary<object, List<ModularAvatarMenuInstaller>>();
|
||||
|
||||
private Dictionary<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>> _installerToTargetComponent
|
||||
= new Dictionary<ModularAvatarMenuInstaller, List<ModularAvatarMenuInstallTarget>>();
|
||||
|
||||
private Dictionary<object, VirtualMenuNode> _resolvedMenu = new Dictionary<object, VirtualMenuNode>();
|
||||
|
||||
// TODO: immutable?
|
||||
public Dictionary<object, VirtualMenuNode> ResolvedMenu => _resolvedMenu;
|
||||
public VirtualMenuNode RootMenuNode => ResolvedMenu[RootMenuKey];
|
||||
|
||||
private Queue<Action> _pendingGeneration = new Queue<Action>();
|
||||
private HashSet<VRCExpressionsMenu> _visitedMenus = new HashSet<VRCExpressionsMenu>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the VirtualMenu.
|
||||
/// </summary>
|
||||
/// <param name="rootMenu">The root VRCExpressionsMenu to import</param>
|
||||
internal VirtualMenu(VRCExpressionsMenu rootMenu)
|
||||
{
|
||||
if (rootMenu != null)
|
||||
{
|
||||
RootMenuKey = rootMenu;
|
||||
}
|
||||
else
|
||||
{
|
||||
RootMenuKey = RootMenu.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
internal IEnumerable<ModularAvatarMenuInstallTarget> GetInstallTargetsForInstaller(
|
||||
ModularAvatarMenuInstaller installer
|
||||
)
|
||||
{
|
||||
if (_installerToTargetComponent.TryGetValue(installer, out var targets))
|
||||
{
|
||||
return targets;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Array.Empty<ModularAvatarMenuInstallTarget>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a menu installer with this virtual menu. Because we need the full set of components indexed to
|
||||
/// determine the effects of this menu installer, further processing is deferred until we freeze the menu.
|
||||
/// </summary>
|
||||
/// <param name="installer"></param>
|
||||
internal void RegisterMenuInstaller(ModularAvatarMenuInstaller installer)
|
||||
{
|
||||
// initial validation
|
||||
if (installer.menuToAppend == null && installer.GetComponent<MenuSource>() == null) return;
|
||||
|
||||
var target = installer.installTargetMenu ? (object) installer.installTargetMenu : RootMenuKey;
|
||||
if (!_targetMenuToInstaller.TryGetValue(target, out var targets))
|
||||
{
|
||||
targets = new List<ModularAvatarMenuInstaller>();
|
||||
_targetMenuToInstaller[target] = targets;
|
||||
}
|
||||
|
||||
targets.Add(installer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an install target with this virtual menu. As with menu installers, processing is delayed.
|
||||
/// </summary>
|
||||
/// <param name="target"></param>
|
||||
internal void RegisterMenuInstallTarget(ModularAvatarMenuInstallTarget target)
|
||||
{
|
||||
if (target.installer == null) return;
|
||||
if (!_installerToTargetComponent.TryGetValue(target.installer, out var targets))
|
||||
{
|
||||
targets = new List<ModularAvatarMenuInstallTarget>();
|
||||
_installerToTargetComponent[target.installer] = targets;
|
||||
}
|
||||
|
||||
targets.Add(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Freezes the menu, fully resolving all members of all menus.
|
||||
/// </summary>
|
||||
internal void FreezeMenu()
|
||||
{
|
||||
ImmutableDictionary<object, ImmutableList<ModularAvatarMenuInstaller>> menuToInstallerFiltered =
|
||||
_targetMenuToInstaller
|
||||
.Select(kvp => new KeyValuePair<object, ImmutableList<ModularAvatarMenuInstaller>>(
|
||||
kvp.Key,
|
||||
kvp.Value.Where(i => !_installerToTargetComponent.ContainsKey(i)).ToImmutableList()
|
||||
))
|
||||
.Where(kvp => !kvp.Value.IsEmpty)
|
||||
.ToImmutableDictionary();
|
||||
|
||||
var RootNode = new VirtualMenuNode(RootMenuKey);
|
||||
_resolvedMenu[RootMenuKey] = RootNode;
|
||||
|
||||
var rootContext =
|
||||
new NodeContextImpl(RootNode, NodeFor, menuToInstallerFiltered, m => _visitedMenus.Add(m));
|
||||
if (RootMenuKey is VRCExpressionsMenu menu)
|
||||
{
|
||||
foreach (var control in menu.controls)
|
||||
{
|
||||
rootContext.PushControl(control);
|
||||
}
|
||||
}
|
||||
|
||||
if (menuToInstallerFiltered.TryGetValue(RootMenuKey, out var installers))
|
||||
{
|
||||
foreach (var installer in installers)
|
||||
{
|
||||
rootContext.PushNode(installer);
|
||||
}
|
||||
}
|
||||
|
||||
while (_pendingGeneration.Count > 0)
|
||||
{
|
||||
_pendingGeneration.Dequeue()();
|
||||
}
|
||||
|
||||
VirtualMenuNode NodeFor(object key)
|
||||
{
|
||||
if (_resolvedMenu.TryGetValue(key, out var node)) return node;
|
||||
node = new VirtualMenuNode(key);
|
||||
_resolvedMenu[key] = node;
|
||||
|
||||
_pendingGeneration.Enqueue(() =>
|
||||
{
|
||||
BuildReport.ReportingObject(key as UnityEngine.Object, () =>
|
||||
{
|
||||
var context = new NodeContextImpl(node, NodeFor, menuToInstallerFiltered,
|
||||
m => _visitedMenus.Add(m));
|
||||
if (key is VRCExpressionsMenu expMenu)
|
||||
{
|
||||
context.PushNode(expMenu);
|
||||
}
|
||||
else if (key is MenuSource source)
|
||||
{
|
||||
context.PushNode(source);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO warning
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
internal VRCExpressionsMenu SerializeMenu(Action<UnityEngine.Object> SaveAsset)
|
||||
{
|
||||
Dictionary<object, VRCExpressionsMenu> serializedMenus = new Dictionary<object, VRCExpressionsMenu>();
|
||||
|
||||
return Serialize(RootMenuKey);
|
||||
|
||||
VRCExpressionsMenu Serialize(object menuKey)
|
||||
{
|
||||
if (menuKey == null) return null;
|
||||
if (serializedMenus.TryGetValue(menuKey, out var menu)) return menu;
|
||||
if (!_resolvedMenu.TryGetValue(menuKey, out var node)) return null;
|
||||
|
||||
menu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
||||
serializedMenus[menuKey] = menu;
|
||||
menu.controls = node.Controls.Select(c =>
|
||||
{
|
||||
var control = new VRCExpressionsMenu.Control();
|
||||
control.name = c.name;
|
||||
control.type = c.type;
|
||||
control.parameter = new VRCExpressionsMenu.Control.Parameter() {name = c.parameter.name};
|
||||
control.value = c.value;
|
||||
control.icon = c.icon;
|
||||
control.style = c.style;
|
||||
control.labels = c.labels.ToArray();
|
||||
control.subParameters = c.subParameters.Select(p => new VRCExpressionsMenu.Control.Parameter()
|
||||
{
|
||||
name = p.name
|
||||
}).ToArray();
|
||||
control.subMenu = Serialize(c.SubmenuNode?.NodeKey);
|
||||
|
||||
return control;
|
||||
}).ToList();
|
||||
|
||||
SaveAsset(menu);
|
||||
|
||||
return menu;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ContainsMenu(VRCExpressionsMenu menu)
|
||||
{
|
||||
return _visitedMenus.Contains(menu);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 70cfde8e889a4057b14153c7021e16c8
|
||||
timeCreated: 1676977210
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using nadena.dev.modular_avatar.core.editor.menu;
|
||||
using nadena.dev.modular_avatar.editor.ErrorReporting;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
@ -20,12 +21,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
private BuildContext _context;
|
||||
|
||||
private Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> _clonedMenus;
|
||||
|
||||
|
||||
private VRCExpressionsMenu _rootMenu;
|
||||
|
||||
private MenuTree _menuTree;
|
||||
private Stack<ModularAvatarMenuInstaller> _visitedInstallerStack;
|
||||
|
||||
public void OnPreprocessAvatar(GameObject avatarRoot, BuildContext context)
|
||||
@ -38,8 +35,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
.ToArray();
|
||||
if (menuInstallers.Length == 0) return;
|
||||
|
||||
|
||||
_clonedMenus = new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
|
||||
_visitedInstallerStack = new Stack<ModularAvatarMenuInstaller>();
|
||||
|
||||
VRCAvatarDescriptor avatar = avatarRoot.GetComponent<VRCAvatarDescriptor>();
|
||||
@ -49,59 +44,19 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
var menu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
||||
_context.SaveAsset(menu);
|
||||
avatar.expressionsMenu = menu;
|
||||
_clonedMenus[menu] = menu;
|
||||
context.ClonedMenus[menu] = menu;
|
||||
}
|
||||
|
||||
_rootMenu = avatar.expressionsMenu;
|
||||
_menuTree = new MenuTree(avatar);
|
||||
_menuTree.TraverseAvatarMenu();
|
||||
|
||||
avatar.expressionsMenu = CloneMenu(avatar.expressionsMenu);
|
||||
|
||||
foreach (ModularAvatarMenuInstaller installer in menuInstallers)
|
||||
var virtualMenu = VirtualMenu.ForAvatar(avatar);
|
||||
avatar.expressionsMenu = virtualMenu.SerializeMenu(asset =>
|
||||
{
|
||||
BuildReport.ReportingObject(installer, () => _menuTree.TraverseMenuInstaller(installer));
|
||||
}
|
||||
|
||||
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(null))
|
||||
{
|
||||
BuildReport.ReportingObject(childElement.installer, () => InstallMenu(childElement.installer));
|
||||
}
|
||||
context.SaveAsset(asset);
|
||||
if (asset is VRCExpressionsMenu menu) SplitMenu(menu);
|
||||
});
|
||||
}
|
||||
|
||||
private void InstallMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu installTarget = null)
|
||||
{
|
||||
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 (!_clonedMenus.TryGetValue(installTarget, 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))
|
||||
{
|
||||
InstallMenu(childElement.installer, childElement.parent);
|
||||
}
|
||||
|
||||
_visitedInstallerStack.Pop();
|
||||
}
|
||||
|
||||
private void SplitMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu targetMenu)
|
||||
private void SplitMenu(VRCExpressionsMenu targetMenu)
|
||||
{
|
||||
while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS)
|
||||
{
|
||||
@ -128,42 +83,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
labels = Array.Empty<VRCExpressionsMenu.Control.Label>()
|
||||
});
|
||||
|
||||
_clonedMenus[installer.installTargetMenu] = newMenu;
|
||||
targetMenu = newMenu;
|
||||
}
|
||||
}
|
||||
|
||||
private VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu)
|
||||
{
|
||||
if (menu == null) return null;
|
||||
if (_clonedMenus.TryGetValue(menu, out var newMenu)) return newMenu;
|
||||
newMenu = Object.Instantiate(menu);
|
||||
_context.SaveAsset(newMenu);
|
||||
_clonedMenus[menu] = newMenu;
|
||||
|
||||
foreach (var control in newMenu.controls)
|
||||
{
|
||||
if (Util.ValidateExpressionMenuIcon(control.icon) != Util.ValidateExpressionMenuIconResult.Success)
|
||||
control.icon = null;
|
||||
|
||||
for (int i = 0; i < control.labels.Length; i++)
|
||||
{
|
||||
var label = control.labels[i];
|
||||
var labelResult = Util.ValidateExpressionMenuIcon(label.icon);
|
||||
if (labelResult != Util.ValidateExpressionMenuIconResult.Success)
|
||||
{
|
||||
label.icon = null;
|
||||
control.labels[i] = label;
|
||||
}
|
||||
}
|
||||
|
||||
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
|
||||
{
|
||||
control.subMenu = CloneMenu(control.subMenu);
|
||||
}
|
||||
}
|
||||
|
||||
return newMenu;
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: effd4557902f4578af42d3bdfb7f876d
|
||||
timeCreated: 1670746991
|
@ -178,6 +178,26 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
public class ModularAvatarMenuGroup : MenuSourceComponent
|
||||
{
|
||||
private bool recursing = false;
|
||||
|
||||
public GameObject targetObject;
|
||||
|
||||
public override void Visit(NodeContext context)
|
||||
{
|
||||
context.PushNode(new MenuNodesUnder(targetObject != null ? targetObject : gameObject));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97e46a47dd8a425eb4ce9411defe313d
|
||||
timeCreated: 1677080023
|
@ -6,15 +6,22 @@ namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
[AddComponentMenu("Modular Avatar/MA Menu Installer")]
|
||||
public class ModularAvatarMenuInstaller : AvatarTagComponent
|
||||
{
|
||||
public VRCExpressionsMenu menuToAppend;
|
||||
{
|
||||
public VRCExpressionsMenu menuToAppend;
|
||||
public VRCExpressionsMenu installTargetMenu;
|
||||
|
||||
|
||||
// ReSharper disable once Unity.RedundantEventFunction
|
||||
void Start()
|
||||
{
|
||||
// Ensure that unity generates an enable checkbox
|
||||
}
|
||||
{
|
||||
// Ensure that unity generates an enable checkbox
|
||||
}
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
RuntimeUtil.InvalidateMenu();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
// Internal runtime API for the Virtual Menu system.
|
||||
//
|
||||
// IMPORTANT: This API is currently considered unstable. Due to C# protection rules, we are required to make classes
|
||||
// here public, but be aware that they may change without warning in the future.
|
||||
namespace nadena.dev.modular_avatar.core.menu
|
||||
{
|
||||
/// <summary>
|
||||
/// A MenuNode represents a single VRCExpressionsMenu, prior to overflow splitting. MenuNodes form a directed graph,
|
||||
/// which may contain cycles, and may include contributions from multiple MenuInstallers, or from the base avatar
|
||||
/// menu.
|
||||
/// </summary>
|
||||
public class VirtualMenuNode
|
||||
{
|
||||
public List<VirtualControl> Controls = new List<VirtualControl>();
|
||||
|
||||
/// <summary>
|
||||
/// The primary (serialized) object that contributed to this menu; if we want to add more items to it, we look
|
||||
/// here. This can currently be either a VRCExpressionsMenu, a MAMenuItem, or a RootMenu.
|
||||
/// </summary>
|
||||
public readonly object NodeKey;
|
||||
|
||||
internal VirtualMenuNode(object nodeKey)
|
||||
{
|
||||
NodeKey = nodeKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A single control on a MenuNode. The main difference between this and a true VRCExpressionsMenu.Control is that
|
||||
* we use a MenuNode instead of a VRCExpressionsMenu for submenus.
|
||||
*/
|
||||
public class VirtualControl : VRCExpressionsMenu.Control
|
||||
{
|
||||
/// <summary>
|
||||
/// VirtualControls do not reference real VRCExpressionsMenu objects, but rather virtual MenuNodes.
|
||||
/// </summary>
|
||||
public VirtualMenuNode SubmenuNode;
|
||||
|
||||
internal VirtualControl(VRCExpressionsMenu.Control control)
|
||||
{
|
||||
this.name = control.name;
|
||||
this.type = control.type;
|
||||
this.parameter = new Parameter() {name = control?.parameter?.name};
|
||||
this.value = control.value;
|
||||
this.icon = control.icon;
|
||||
this.style = control.style;
|
||||
this.subMenu = null;
|
||||
this.subParameters = control.subParameters?.Select(p => new VRCExpressionsMenu.Control.Parameter()
|
||||
{
|
||||
name = p.name
|
||||
})?.ToArray();
|
||||
this.labels = control.labels?.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper MenuSource which includes all children of a given GameObject containing MenuSourceComponents as menu
|
||||
/// items. Implements equality based on the GameObject in question.
|
||||
/// </summary>
|
||||
internal class MenuNodesUnder : MenuSource
|
||||
{
|
||||
internal readonly GameObject root;
|
||||
|
||||
public MenuNodesUnder(GameObject root)
|
||||
{
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj)) return false;
|
||||
if (ReferenceEquals(this, obj)) return true;
|
||||
if (obj.GetType() != this.GetType()) return false;
|
||||
return root == ((MenuNodesUnder) obj).root;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (root != null ? root.GetHashCode() : 0);
|
||||
}
|
||||
|
||||
public void Visit(NodeContext context)
|
||||
{
|
||||
foreach (Transform t in root.transform)
|
||||
{
|
||||
var source = t.GetComponent<MenuSourceComponent>();
|
||||
if (source != null) context.PushNode(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The NodeContext provides callbacks for MenuSource visitors to append controls and/or other node types to a menu
|
||||
/// node.
|
||||
/// </summary>
|
||||
public interface NodeContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Pushes the contents of this expressions menu asset onto the current menu node, handling loops and menu
|
||||
/// installer invocations.
|
||||
/// </summary>
|
||||
/// <param name="expMenu"></param>
|
||||
void PushNode(VRCExpressionsMenu expMenu);
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the contents of this menu source onto the current menu node.
|
||||
/// </summary>
|
||||
/// <param name="source"></param>
|
||||
void PushNode(MenuSource source);
|
||||
|
||||
/// <summary>
|
||||
/// Pushes this menu installer onto this node
|
||||
/// </summary>
|
||||
/// <param name="installer"></param>
|
||||
void PushNode(ModularAvatarMenuInstaller installer);
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a single expressions menu control onto the current menu node. Converts submenus into menu nodes
|
||||
/// automatically.
|
||||
/// </summary>
|
||||
/// <param name="control"></param>
|
||||
void PushControl(VRCExpressionsMenu.Control control);
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a single expressions menu control onto the current menu node.
|
||||
/// </summary>
|
||||
/// <param name="control"></param>
|
||||
void PushControl(VirtualControl control);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the menu node for a given VRCExpressionsMenu asset. This node may not be populated at the time this
|
||||
/// node returns.
|
||||
/// </summary>
|
||||
/// <param name="menu"></param>
|
||||
/// <returns></returns>
|
||||
VirtualMenuNode NodeFor(VRCExpressionsMenu menu);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the menu node for a given menu source asset. The contents of the node may not yet be populated.
|
||||
/// </summary>
|
||||
/// <param name="menu"></param>
|
||||
/// <returns></returns>
|
||||
VirtualMenuNode NodeFor(MenuSource menu);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An object which can contribute controls to a menu.
|
||||
/// </summary>
|
||||
public interface MenuSource
|
||||
{
|
||||
void Visit(NodeContext context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A component which can be used to generate menu items.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public abstract class MenuSourceComponent : AvatarTagComponent, MenuSource
|
||||
{
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
RuntimeUtil.InvalidateMenu();
|
||||
}
|
||||
|
||||
public abstract void Visit(NodeContext context);
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1defe88300684bc38ee24944075d540e
|
||||
timeCreated: 1677147255
|
@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
using VRC.SDKBase;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
/// <summary>
|
||||
/// The menu install target includes the controls of the target menu installer at the point of reference.
|
||||
/// Notably, this can include multiple controls.
|
||||
///
|
||||
/// One tricky aspect of this feature is that we need to disambiguate when a menu installer also cites a target menu.
|
||||
/// Generally, if an installer is targeted by any menu install target (even if - especially if - disabled), we
|
||||
/// ignore its install target configuration entirely.
|
||||
///
|
||||
/// We can also end up with a loop between install targets; in this case, we break the loop at an arbitrary point.
|
||||
/// </summary>
|
||||
internal class ModularAvatarMenuInstallTarget : MenuSourceComponent
|
||||
{
|
||||
public ModularAvatarMenuInstaller installer;
|
||||
|
||||
public override void Visit(NodeContext context)
|
||||
{
|
||||
context.PushNode(installer);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fad1419b52a42ae89b0df52eb861e47
|
||||
timeCreated: 1676976513
|
@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using nadena.dev.modular_avatar.core.menu;
|
||||
using UnityEngine;
|
||||
using VRC.SDK3.Avatars.ScriptableObjects;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
public enum SubmenuSource
|
||||
{
|
||||
MenuAsset,
|
||||
Children,
|
||||
}
|
||||
|
||||
[AddComponentMenu("Modular Avatar/MA Menu Item")]
|
||||
public class ModularAvatarMenuItem : MenuSourceComponent
|
||||
{
|
||||
public VRCExpressionsMenu.Control Control;
|
||||
public SubmenuSource MenuSource;
|
||||
|
||||
public GameObject menuSource_otherObjectChildren;
|
||||
|
||||
public override void Visit(NodeContext context)
|
||||
{
|
||||
var cloned = new VirtualControl(Control);
|
||||
cloned.subMenu = null;
|
||||
cloned.name = gameObject.name;
|
||||
|
||||
if (cloned.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
|
||||
{
|
||||
switch (this.MenuSource)
|
||||
{
|
||||
case SubmenuSource.MenuAsset:
|
||||
cloned.SubmenuNode = context.NodeFor(this.Control.subMenu);
|
||||
break;
|
||||
case SubmenuSource.Children:
|
||||
{
|
||||
var root = this.menuSource_otherObjectChildren != null
|
||||
? this.menuSource_otherObjectChildren
|
||||
: this.gameObject;
|
||||
|
||||
cloned.SubmenuNode = context.NodeFor(new MenuNodesUnder(root));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.PushControl(cloned);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b29d45007c5493d926d2cd45a489529
|
||||
timeCreated: 1676787152
|
@ -39,6 +39,13 @@ namespace nadena.dev.modular_avatar.core
|
||||
public static Action<Action> delayCall = (_) => { };
|
||||
public static event Action OnHierarchyChanged;
|
||||
|
||||
internal static event Action OnMenuInvalidate;
|
||||
|
||||
internal static void InvalidateMenu()
|
||||
{
|
||||
OnMenuInvalidate?.Invoke();
|
||||
}
|
||||
|
||||
public enum OnDemandSource
|
||||
{
|
||||
Awake,
|
||||
|
@ -11,6 +11,7 @@ GameObject:
|
||||
- component: {fileID: 2929086332187968543}
|
||||
- component: {fileID: 7996665789334891805}
|
||||
- component: {fileID: 7184458914139497616}
|
||||
- component: {fileID: 4714073180460761108}
|
||||
m_Layer: 0
|
||||
m_Name: Fingerpen
|
||||
m_TagString: Untagged
|
||||
@ -30,6 +31,7 @@ Transform:
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children:
|
||||
- {fileID: 6917936183219720762}
|
||||
- {fileID: 728513151284643908}
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
@ -74,6 +76,32 @@ MonoBehaviour:
|
||||
syncType: 3
|
||||
defaultValue: 0
|
||||
saved: 0
|
||||
--- !u!114 &4714073180460761108
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2929086332187968542}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3b29d45007c5493d926d2cd45a489529, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
Control:
|
||||
name:
|
||||
icon: {fileID: 0}
|
||||
type: 103
|
||||
parameter:
|
||||
name:
|
||||
value: 1
|
||||
style: 0
|
||||
subMenu: {fileID: 0}
|
||||
subParameters: []
|
||||
labels: []
|
||||
MenuSource: 1
|
||||
menuSource_installer: {fileID: 0}
|
||||
menuSource_otherObjectChildren: {fileID: 5354519745013217104}
|
||||
--- !u!1 &2929086333654575795
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@ -250,6 +278,95 @@ TrailRenderer:
|
||||
m_MinVertexDistance: 0.005
|
||||
m_Autodestruct: 0
|
||||
m_Emitting: 0
|
||||
--- !u!1 &5354519745013217104
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 728513151284643908}
|
||||
m_Layer: 0
|
||||
m_Name: Menu
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &728513151284643908
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5354519745013217104}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children:
|
||||
- {fileID: 1892928965174260857}
|
||||
- {fileID: 3041863097553433968}
|
||||
m_Father: {fileID: 2929086332187968543}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &6837276917073475290
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1892928965174260857}
|
||||
- component: {fileID: 1436771406052350457}
|
||||
m_Layer: 0
|
||||
m_Name: Enable
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1892928965174260857
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6837276917073475290}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children: []
|
||||
m_Father: {fileID: 728513151284643908}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1436771406052350457
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6837276917073475290}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3b29d45007c5493d926d2cd45a489529, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
Control:
|
||||
name:
|
||||
icon: {fileID: 0}
|
||||
type: 102
|
||||
parameter:
|
||||
name: Enable
|
||||
value: 1
|
||||
style: 0
|
||||
subMenu: {fileID: 0}
|
||||
subParameters: []
|
||||
labels: []
|
||||
MenuSource: 0
|
||||
menuSource_installer: {fileID: 0}
|
||||
menuSource_otherObjectChildren: {fileID: 0}
|
||||
--- !u!1 &7502135110828725241
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@ -319,6 +436,63 @@ Animator:
|
||||
m_HasTransformHierarchy: 1
|
||||
m_AllowConstantClipSamplingOptimization: 1
|
||||
m_KeepAnimatorControllerStateOnDisable: 0
|
||||
--- !u!1 &8323130273213051223
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 3041863097553433968}
|
||||
- component: {fileID: 1720968870752482547}
|
||||
m_Layer: 0
|
||||
m_Name: Draw
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &3041863097553433968
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8323130273213051223}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_Children: []
|
||||
m_Father: {fileID: 728513151284643908}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1720968870752482547
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 8323130273213051223}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3b29d45007c5493d926d2cd45a489529, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
Control:
|
||||
name:
|
||||
icon: {fileID: 0}
|
||||
type: 101
|
||||
parameter:
|
||||
name: Draw
|
||||
value: 1
|
||||
style: 0
|
||||
subMenu: {fileID: 0}
|
||||
subParameters: []
|
||||
labels: []
|
||||
MenuSource: 0
|
||||
menuSource_installer: {fileID: 0}
|
||||
menuSource_otherObjectChildren: {fileID: 0}
|
||||
--- !u!1 &9091324582054793565
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
@ -1,25 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: -340790334, guid: 67cc4cb7839cd3741b63733d5adf0442, type: 3}
|
||||
m_Name: Fingerpen
|
||||
m_EditorClassIdentifier:
|
||||
controls:
|
||||
- name: Pen
|
||||
icon: {fileID: 0}
|
||||
type: 103
|
||||
parameter:
|
||||
name:
|
||||
value: 1
|
||||
style: 0
|
||||
subMenu: {fileID: 11400000, guid: 1d1f3d0e712c1394d9623677363da960, type: 2}
|
||||
subParameters: []
|
||||
labels: []
|
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c9e45343a49f484ba069e09ccced597
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -1,35 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: -340790334, guid: 67cc4cb7839cd3741b63733d5adf0442, type: 3}
|
||||
m_Name: FingerpenSub
|
||||
m_EditorClassIdentifier:
|
||||
controls:
|
||||
- name: Enable
|
||||
icon: {fileID: 0}
|
||||
type: 102
|
||||
parameter:
|
||||
name: Enable
|
||||
value: 1
|
||||
style: 0
|
||||
subMenu: {fileID: 0}
|
||||
subParameters: []
|
||||
labels: []
|
||||
- name: Draw
|
||||
icon: {fileID: 0}
|
||||
type: 101
|
||||
parameter:
|
||||
name: Draw
|
||||
value: 1
|
||||
style: 0
|
||||
subMenu: {fileID: 0}
|
||||
subParameters: []
|
||||
labels: []
|
@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d1f3d0e712c1394d9623677363da960
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -158,6 +158,13 @@
|
||||
"com.unity.nuget.newtonsoft-json": "2.0.2"
|
||||
}
|
||||
},
|
||||
"de.thryrallo.vrc.avatar-performance-tools": {
|
||||
"version": "https://github.com/Thryrallo/VRC-Avatar-Performance-Tools.git",
|
||||
"depth": 0,
|
||||
"source": "git",
|
||||
"dependencies": {},
|
||||
"hash": "ba9c16e482e7a376db18e9a85e24fabc04649d37"
|
||||
},
|
||||
"nadena.dev.modular-avatar": {
|
||||
"version": "file:nadena.dev.modular-avatar",
|
||||
"depth": 0,
|
||||
|
@ -9,13 +9,14 @@
|
||||
},
|
||||
"locked": {
|
||||
"com.vrchat.avatars": {
|
||||
"version": "3.1.10",
|
||||
"version": "3.1.11",
|
||||
"dependencies": {
|
||||
"com.vrchat.base": "3.1.x"
|
||||
"com.vrchat.base": "3.1.11"
|
||||
}
|
||||
},
|
||||
"com.vrchat.base": {
|
||||
"version": "3.1.10"
|
||||
"version": "3.1.11",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.vrchat.core.vpm-resolver": {
|
||||
"version": "0.1.17"
|
||||
|
Loading…
Reference in New Issue
Block a user