chore: Create toggle with submenu in "CreateToggleForSelection" ()

* chore: refactor and Ignore GameObjects with submenu for CreateToggleForSelection

* chore: create toggle with submenu in CreateToggleForSelection

* chore: use simple suffixes

* chore: update CHANGELOG

* fix: update CHANGELOG

---------

Co-authored-by: bd_ <bd_@nadena.dev>
This commit is contained in:
Ao_425 2025-03-15 12:54:15 +09:00 committed by GitHub
parent 98311f11f8
commit fc9b2683c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 155 additions and 17 deletions

@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 に対してはWrite Defaultsを調整しないように変更。  に対してはWrite Defaultsを調整しないように変更。
- [#1429] Merge Armature は、特定の場合にPhysBoneに指定されたヒューマイドボーンをマージできるようになりました。 - [#1429] Merge Armature は、特定の場合にPhysBoneに指定されたヒューマイドボーンをマージできるようになりました。
- 具体的には、子ヒューマイドボーンがある場合はPhysBoneから除外される必要があります。 - 具体的には、子ヒューマイドボーンがある場合はPhysBoneから除外される必要があります。
- [#1437] Create Toggle for Selectionにおいて、複数選択時時に必要に応じてサブメニューを生成し、子としてトグルを生成するように変更されました。
- [#1499] `Object Toggle`で制御される`Audio Source`がアニメーションブロックされたときに常にアクティブにならないように、 - [#1499] `Object Toggle`で制御される`Audio Source`がアニメーションブロックされたときに常にアクティブにならないように、
アニメーションがブロックされたときにオーディオソースを無効にするように変更。 アニメーションがブロックされたときにオーディオソースを無効にするように変更。
- [#1489] `Merge Blend Tree` やリアクティブコンポーネントとMMDワールドの互換性の問題を修正。 - [#1489] `Merge Blend Tree` やリアクティブコンポーネントとMMDワールドの互換性の問題を修正。

@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
additive layers, or layers with only one state and no transitions. additive layers, or layers with only one state and no transitions.
- [#1429] Merge Armature will now allow you to merge humanoid bones with PhysBones attached in certain cases. - [#1429] Merge Armature will now allow you to merge humanoid bones with PhysBones attached in certain cases.
- Specifically, child humanoid bones (if there are any) must be excluded from all attached Physbones. - Specifically, child humanoid bones (if there are any) must be excluded from all attached Physbones.
- [#1437] Create Toggle for Selection now creates submenus as necessary when multiple items are selected, and creates toggles as children.
- [#1499] When an audio source is controlled by an Object Toggle, disable the audio source when animations are blocked - [#1499] When an audio source is controlled by an Object Toggle, disable the audio source when animations are blocked
to avoid it unintentionally being constantly active. to avoid it unintentionally being constantly active.

@ -25,6 +25,7 @@ Modular Avatarの主な変更点をこのファイルで記録しています。
 に対してはWrite Defaultsを調整しないように変更。  に対してはWrite Defaultsを調整しないように変更。
- [#1429] Merge Armature は、特定の場合にPhysBoneに指定されたヒューマイドボーンをマージできるようになりました。 - [#1429] Merge Armature は、特定の場合にPhysBoneに指定されたヒューマイドボーンをマージできるようになりました。
- 具体的には、子ヒューマイドボーンがある場合はPhysBoneから除外される必要があります。 - 具体的には、子ヒューマイドボーンがある場合はPhysBoneから除外される必要があります。
- [#1437] Create Toggle for Selectionにおいて、複数選択時時に必要に応じてサブメニューを生成し、子としてトグルを生成するように変更されました。
- [#1499] `Object Toggle`で制御される`Audio Source`がアニメーションブロックされたときに常にアクティブにならないように、 - [#1499] `Object Toggle`で制御される`Audio Source`がアニメーションブロックされたときに常にアクティブにならないように、
アニメーションがブロックされたときにオーディオソースを無効にするように変更。 アニメーションがブロックされたときにオーディオソースを無効にするように変更。

@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
additive layers, or layers with only one state and no transitions. additive layers, or layers with only one state and no transitions.
- [#1429] Merge Armature will now allow you to merge humanoid bones with PhysBones attached in certain cases. - [#1429] Merge Armature will now allow you to merge humanoid bones with PhysBones attached in certain cases.
- Specifically, child humanoid bones (if there are any) must be excluded from all attached Physbones. - Specifically, child humanoid bones (if there are any) must be excluded from all attached Physbones.
- [#1437] Create Toggle for Selection now creates submenus as necessary when multiple items are selected, and creates toggles as children.
- [#1499] When an audio source is controlled by an Object Toggle, disable the audio source when animations are blocked - [#1499] When an audio source is controlled by an Object Toggle, disable the audio source when animations are blocked
to avoid it unintentionally being constantly active. to avoid it unintentionally being constantly active.

@ -1,4 +1,7 @@
#if MA_VRCSDK3_AVATARS #if MA_VRCSDK3_AVATARS
using System;
using System.Linq;
using System.Collections.Generic;
using nadena.dev.modular_avatar.ui; using nadena.dev.modular_avatar.ui;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
@ -9,39 +12,170 @@ namespace nadena.dev.modular_avatar.core.editor
internal static class ToggleCreatorShortcut internal static class ToggleCreatorShortcut
{ {
[MenuItem(UnityMenuItems.GameObject_CreateToggleForSelection, false, UnityMenuItems.GameObject_CreateToggleForSelectionOrder)] [MenuItem(UnityMenuItems.GameObject_CreateToggleForSelection, false, UnityMenuItems.GameObject_CreateToggleForSelectionOrder)]
private static void CreateToggleForSelection(MenuCommand command) => CreateToggleImpl(command, true); private static void CreateToggleForSelection()
{
var forSelection = true;
var selections = Selection.objects.OfType<GameObject>();
// Ignore GameObjects with submenu in the context of CreateToggleForSelection.
selections = selections.Where(s => !TryGetChildrenSourceSubmenu(s, out var _));
if (selections.Count() == 0) return;
// Grouping according to parent
var groups = new Dictionary<GameObject, HashSet<GameObject>>();
foreach (var selected in selections)
{
var parent = selected.transform.parent?.gameObject;
if (parent == null) continue;
if (!groups.ContainsKey(parent))
{
groups[parent] = new();
}
groups[parent].Add(selected);
}
foreach (var group in groups)
{
var parent = group.Key;
var targets = group.Value;
if (parent == null) continue;
if (targets == null || targets.Count() == 0) continue;
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(parent.transform);
if (avatarRoot == null) continue;
var subMenuName = parent.name + " Toggles";
// Try to find target submenu that should be the parent of toggles
ModularAvatarMenuItem targetSubMenu = null;
if (TryGetChildrenSourceSubmenu(parent, out var subMenu))
{
// If parent has subMenu, use it as target submenu.
targetSubMenu = subMenu;
}
else
{
// If parent hasn't subMenu, get submenus at the same level
var subMenus = new List<ModularAvatarMenuItem>();
foreach (Transform sibling in parent.transform)
{
if (TryGetChildrenSourceSubmenu(sibling.gameObject, out var m)) { subMenus.Add(m); }
}
// Filter to submenus with the same name
subMenus = subMenus.Where(m => m.gameObject.name == subMenuName).ToList();
// If only one submenu as target is found, use it as target submenu.
if (subMenus.Count() == 1) targetSubMenu = subMenus.First();
}
if (targetSubMenu != null) // If target SubMenu is found, add the toggles as children of it.
{
parent = targetSubMenu.gameObject;
CreateToggleImpl(targets, parent, forSelection, createInstaller:false);
}
else
{
if (targets.Count() > 1) // Create a submenu and add the toggles as children of it.
{
parent = CreateSubMenu(parent, subMenuName).gameObject;
CreateToggleImpl(targets, parent, forSelection, createInstaller:false);
}
else // Create a single toggle with installer.
{
var target = targets.First();
CreateToggleImpl(target, parent, forSelection, createInstaller:true);
}
}
}
Selection.objects = null;
}
[MenuItem(UnityMenuItems.GameObject_CreateToggle, false, UnityMenuItems.GameObject_CreateToggleOrder)] [MenuItem(UnityMenuItems.GameObject_CreateToggle, false, UnityMenuItems.GameObject_CreateToggleOrder)]
private static void CreateToggle(MenuCommand command) => CreateToggleImpl(command, false); private static void CreateToggle()
private static void CreateToggleImpl(MenuCommand command, bool forSelection)
{ {
var selected = command.context as GameObject; var selections = Selection.objects.OfType<GameObject>();
if (selected == null) return; if (selections.Count() == 0) return;
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform); foreach (var selected in selections)
if (avatarRoot == null) return; {
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
if (avatarRoot == null) return;
bool createInstaller = true; var parent = avatarRoot.gameObject;
Transform parent = avatarRoot; var createInstaller = true;
if (TryGetChildrenSourceSubmenu(selected, out var _))
{
parent = selected;
createInstaller = false;
}
CreateToggleImpl(selected, parent, createInstaller:createInstaller);
}
Selection.objects = null;
}
private static bool TryGetChildrenSourceSubmenu(GameObject target, out ModularAvatarMenuItem subMenu)
{
subMenu = null;
try try
{ {
var selectedMenuItem = selected.GetComponent<ModularAvatarMenuItem>(); var mami = target.GetComponent<ModularAvatarMenuItem>();
if (selectedMenuItem?.Control?.type == VRCExpressionsMenu.Control.ControlType.SubMenu if (mami?.Control?.type == VRCExpressionsMenu.Control.ControlType.SubMenu
&& selectedMenuItem.MenuSource == SubmenuSource.Children && mami.MenuSource == SubmenuSource.Children
) )
{ {
parent = selected.transform; subMenu = mami;
createInstaller = false; return true;
} }
} }
catch (MissingComponentException) catch (MissingComponentException)
{ {
// ignore // ignore
} }
return false;
}
private static ModularAvatarMenuItem CreateSubMenu(GameObject parent, string submenuname)
{
var submenu = new GameObject(submenuname);
submenu.transform.SetParent(parent.transform);
var name = forSelection ? selected.name + " Toggle" : "New Toggle"; var mami = submenu.AddComponent<ModularAvatarMenuItem>();
mami.InitSettings();
mami.Control = new VRCExpressionsMenu.Control
{
type = VRCExpressionsMenu.Control.ControlType.SubMenu,
name = submenuname,
};
submenu.AddComponent<ModularAvatarMenuInstaller>();
Selection.activeGameObject = submenu;
EditorGUIUtility.PingObject(submenu);
Undo.RegisterCreatedObjectUndo(submenu, "Create SubMenu");
return mami;
}
private static void CreateToggleImpl(IEnumerable<GameObject> selections, GameObject parent, bool forSelection = false, bool createInstaller = true)
{
foreach (var selected in selections)
{
CreateToggleImpl(selected, parent, forSelection, createInstaller);
}
}
private static void CreateToggleImpl(GameObject selected, GameObject parent, bool forSelection = false, bool createInstaller = true)
{
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(selected.transform);
if (avatarRoot == null) return;
var suffix = selected.activeSelf ? "OFF" : "ON";
var name = forSelection ? $"{selected.name} {suffix}" : "New Toggle";
var toggle = new GameObject(name); var toggle = new GameObject(name);
@ -57,7 +191,7 @@ namespace nadena.dev.modular_avatar.core.editor
} }
toggle.transform.SetParent(parent, false); toggle.transform.SetParent(parent.transform, false);
var mami = toggle.AddComponent<ModularAvatarMenuItem>(); var mami = toggle.AddComponent<ModularAvatarMenuItem>();
mami.InitSettings(); mami.InitSettings();