From b6537da650f806ecf6ded90d6afa996c982542a2 Mon Sep 17 00:00:00 2001 From: bd_ Date: Fri, 8 Sep 2023 19:42:16 +0900 Subject: [PATCH] ui: improve setup outfit usability (#422) Show a descriptive error when setup outfit fails (fixes #415). Use HeuristicBoneMapper to fuzzy-match hips (fixes #414) --- .../Editor/EasySetupOutfit.cs | 192 +++++++++++++++++- .../Editor/Localization/Localization.cs | 12 ++ .../Editor/Localization/en.json | 11 +- .../Editor/Localization/ja.json | 11 +- 4 files changed, 215 insertions(+), 11 deletions(-) diff --git a/Packages/nadena.dev.modular-avatar/Editor/EasySetupOutfit.cs b/Packages/nadena.dev.modular-avatar/Editor/EasySetupOutfit.cs index 76f9c922..00c1582e 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/EasySetupOutfit.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/EasySetupOutfit.cs @@ -1,15 +1,119 @@ -using UnityEditor; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; using UnityEngine; +using Object = UnityEngine.Object; +using static nadena.dev.modular_avatar.core.editor.Localization; namespace nadena.dev.modular_avatar.core.editor { - public class EasySetupOutfit + internal class ESOErrorWindow : EditorWindow + { + private string header; + private string[] messageGroups; + private static readonly GUIStyle buttonStyle, labelStyle; + private const float SeparatorSize = 6f; + + static ESOErrorWindow() + { + buttonStyle = EditorStyles.miniButtonRight; + labelStyle = EditorStyles.label; + labelStyle.wordWrap = true; + + buttonStyle.fixedWidth = 40f; + buttonStyle.fixedHeight = EditorGUIUtility.singleLineHeight * 1.5f; + } + + private void OnEnable() + { + + } + + internal static void Show( + string header, + string[] messageGroups + ) + { + var window = CreateInstance(); + window.titleContent = new GUIContent("Setup Outfit"); + window.header = header; + window.messageGroups = messageGroups; + + // Compute required window size + var height = 0f; + var width = 450f; + + height += SeparatorSize; + height += EditorStyles.helpBox.CalcHeight(new GUIContent(header), width); + foreach (var message in messageGroups) + { + height += 6f; // TODO: constant + height += labelStyle.CalcHeight(new GUIContent(message), width); + } + + height += buttonStyle.fixedHeight; + height += SeparatorSize; + + window.minSize = new Vector2(width, height); + + window.ShowModal(); + } + + private void OnGUI() + { + EditorGUILayout.Space(SeparatorSize); + + EditorGUILayout.HelpBox(header, MessageType.Error); + + foreach (var message in messageGroups) + { + EditorGUILayout.Space(SeparatorSize); + EditorGUILayout.LabelField(message); + } + + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("OK", buttonStyle)) + { + Close(); + } + EditorGUILayout.EndHorizontal(); + + var finalRect = GUILayoutUtility.GetRect(SeparatorSize, SeparatorSize, GUILayout.ExpandWidth(true)); + + var size = this.minSize; + size.y = finalRect.position.y + finalRect.height; + + if (size.y > 10) + { + if (Vector2.Distance(this.minSize, size) > 1f) + { + this.minSize = size; + } + + if (Vector2.Distance(this.maxSize, size) > 1f) + { + this.maxSize = size; + } + } + } + } + + internal class EasySetupOutfit { private const int PRIORITY = 49; + private static string[] errorMessageGroups; + private static string errorHeader; [MenuItem("GameObject/ModularAvatar/Setup Outfit", false, PRIORITY)] static void SetupOutfit(MenuCommand cmd) { + if (!ValidateSetupOutfit()) + { + ESOErrorWindow.Show(errorHeader, errorMessageGroups); + return; + } + if (!FindBones(cmd.context, out var avatarRoot, out var avatarHips, out var outfitHips) ) return; @@ -117,13 +221,21 @@ namespace nadena.dev.modular_avatar.core.editor } } - [MenuItem("GameObject/ModularAvatar/Setup Outfit", true, PRIORITY)] static bool ValidateSetupOutfit() { - if (Selection.objects.Length == 0) return false; + errorHeader = S("setup_outfit.err.header.notarget"); + errorMessageGroups = new string[] { S("setup_outfit.err.unknown") }; + + if (Selection.objects.Length == 0) + { + errorMessageGroups = new string[] { S("setup_outfit.err.no_selection") }; + return false; + } foreach (var obj in Selection.objects) { + errorHeader = S_f("setup_outfit.err.header", obj.name); + if (!(obj is GameObject gameObj)) return false; var xform = gameObj.transform; @@ -137,10 +249,22 @@ namespace nadena.dev.modular_avatar.core.editor // refusing to run if we detect multiple avatar descriptors above the current object (or if we're run on // the avdesc object itself) var nearestAvatar = RuntimeUtil.FindAvatarInParents(xform); - if (nearestAvatar == null || nearestAvatar.transform == xform) return false; + if (nearestAvatar == null || nearestAvatar.transform == xform) + { + errorMessageGroups = new string[] + {S_f("setup_outfit.err.multiple_avatar_descriptors", xform.gameObject.name)}; + return false; + } var parent = nearestAvatar.transform.parent; - if (parent != null && RuntimeUtil.FindAvatarInParents(parent) != null) return false; + if (parent != null && RuntimeUtil.FindAvatarInParents(parent) != null) + { + errorMessageGroups = new string[] + { + S_f("setup_outfit.err.no_avatar_descriptor", xform.gameObject.name) + }; + return false; + } } return true; @@ -157,10 +281,24 @@ namespace nadena.dev.modular_avatar.core.editor if (outfitRoot == null || avatarRoot == null) return false; var avatarAnimator = avatarRoot.GetComponent(); - if (avatarAnimator == null) return false; + if (avatarAnimator == null) + { + errorMessageGroups = new string[] + { + S("setup_outfit.err.no_animator") + }; + return false; + } avatarHips = avatarAnimator.GetBoneTransform(HumanBodyBones.Hips)?.gameObject; - if (avatarHips == null) return false; + if (avatarHips == null) + { + errorMessageGroups = new string[] + { + S("setup_outfit.err.no_hips") + }; + return false; + } var outfitAnimator = outfitRoot.GetComponent(); if (outfitAnimator != null) @@ -168,9 +306,12 @@ namespace nadena.dev.modular_avatar.core.editor outfitHips = outfitAnimator.GetBoneTransform(HumanBodyBones.Hips)?.gameObject; } + var hipsCandidates = new List(); + if (outfitHips == null) { - // Heuristic search - usually there'll be root -> Armature -> (single child) Hips + // Heuristic search - usually there'll be root -> Armature -> (single child) Hips. + // First, look for an exact match. foreach (Transform child in outfitRoot.transform) { foreach (Transform tempHip in child) @@ -181,6 +322,39 @@ namespace nadena.dev.modular_avatar.core.editor } } } + hipsCandidates.Add(avatarHips.name); + + // If that doesn't work out, we'll check for heuristic bone mapper mappings. + foreach (var hbm in HeuristicBoneMapper.BoneToNameMap[HumanBodyBones.Hips]) + { + if (hipsCandidates[0] != hbm) + { + hipsCandidates.Add(hbm); + } + } + + foreach (Transform child in outfitRoot.transform) + { + foreach (Transform tempHip in child) + { + foreach (var candidate in hipsCandidates) + { + if (HeuristicBoneMapper.NormalizeName(tempHip.name).Contains(candidate)) + { + outfitHips = tempHip.gameObject; + } + } + } + } + } + + if (outfitHips == null) + { + errorMessageGroups = new string[] + { + S("setup_outfit.err.no_outfit_hips"), + string.Join("\n", hipsCandidates.Select(c => "・ " + c).ToArray()) + }; } return avatarHips != null && outfitHips != null; diff --git a/Packages/nadena.dev.modular-avatar/Editor/Localization/Localization.cs b/Packages/nadena.dev.modular-avatar/Editor/Localization/Localization.cs index aa815ce6..e50dea93 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Localization/Localization.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Localization/Localization.cs @@ -92,6 +92,18 @@ namespace nadena.dev.modular_avatar.core.editor { return S(key, key); } + + public static string S_f(string key, params string[] format) + { + try + { + return string.Format(S(key, key), format); + } + catch (FormatException e) + { + return S(key, key) + "(" + string.Join(", ", format) + ")"; + } + } public static string S(string key, string defValue) { diff --git a/Packages/nadena.dev.modular-avatar/Editor/Localization/en.json b/Packages/nadena.dev.modular-avatar/Editor/Localization/en.json index c525b97f..a9fa0b84 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Localization/en.json +++ b/Packages/nadena.dev.modular-avatar/Editor/Localization/en.json @@ -151,5 +151,14 @@ "animation_gen.duplicate_binding": "Controls from different control groups are trying to animate the same parameter. Parameter: {0}", "animation_gen.multiple_defaults": "Multiple default menu items were found in the same control group.", "menuitem.misc.add_item": "Add menu item", - "replace_object.target_object": "Object to replace" + "replace_object.target_object": "Object to replace", + "setup_outfit.err.header.notarget": "Setup outfit failed", + "setup_outfit.err.header": "Setup Outfit failed to process {0}", + "setup_outfit.err.unknown": "Unknown error", + "setup_outfit.err.no_selection": "No object selected.", + "setup_outfit.err.multiple_avatar_descriptors": "Multiple avatar descriptors found in {0} and its parents.", + "setup_outfit.err.no_avatar_descriptor": "No avatar descriptor found in {0} or its parents.", + "setup_outfit.err.no_animator": "Your avatar does not have an Animator component.", + "setup_outfit.err.no_hips": "Your avatar does not have a Hips bone. Setup Outfit only works on humanoid avatars.", + "setup_outfit.err.no_outfit_hips": "Unable to identify the Hips object for the outfit. Searched for objects containing the following names:" } \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Localization/ja.json b/Packages/nadena.dev.modular-avatar/Editor/Localization/ja.json index 871f9ace..3282beb1 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Localization/ja.json +++ b/Packages/nadena.dev.modular-avatar/Editor/Localization/ja.json @@ -149,5 +149,14 @@ "animation_gen.duplicate_binding": "別々のコントロールグループから、同じパラメーターが操作されています。パラメーター:{0}", "animation_gen.multiple_defaults": "同じコントロールグループに初期設定に指定されたメニューアイテムが複数あります。", "menuitem.misc.add_item": "メニューアイテムを追加", - "replace_object.target_object": "上書き先" + "replace_object.target_object": "上書き先", + "setup_outfit.err.header.notarget": "Setup outfit の処理が失敗しました", + "setup_outfit.err.header": "Setup outfit が「{0}」を処理中に失敗しました。", + "setup_outfit.err.unknown": "原因不明のエラーが発生しました。", + "setup_outfit.err.no_selection": "オブジェクトが選択されていません。", + "setup_outfit.err.multiple_avatar_descriptors": "「{}」とその親に、複数のavatar descriptorを発見しました。", + "setup_outfit.err.no_avatar_descriptor": "「{}」とその親に、avatar descriptorが見つかりませんでした。", + "setup_outfit.err.no_animator": "アバターにAnimatorコンポーネントがありません。", + "setup_outfit.err.no_hips": "アバターにHipsボーンがありません。なお、Setup Outfitはヒューマノイドアバター以外には対応していません。", + "setup_outfit.err.no_outfit_hips": "衣装のHipsボーンを発見できませんでした。以下の名前を含むボーンを探しました:" }