diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs index 06de3fcb..f7457e41 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/BlendshapeSyncEditor.cs @@ -38,8 +38,9 @@ namespace nadena.dev.modular_avatar.core.editor if (_window != null) DestroyImmediate(_window); } - private void OnDestroy() + protected virtual void OnDestroy() { + base.OnDestroy(); if (_window != null) DestroyImmediate(_window); } diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common.meta new file mode 100644 index 00000000..c8d71cfe --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c4bd1da0ca9146e6b4ae77a50ff220f2 +timeCreated: 1693723243 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/LanguageSwitcherElement.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/LanguageSwitcherElement.cs new file mode 100644 index 00000000..557f8c77 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/LanguageSwitcherElement.cs @@ -0,0 +1,22 @@ +using UnityEngine.UIElements; + +namespace nadena.dev.modular_avatar.core.editor +{ + public class LanguageSwitcherElement : VisualElement + { + public new class UxmlFactory : UxmlFactory + { + } + + public new class UxmlTraits : VisualElement.UxmlTraits + { + } + + public LanguageSwitcherElement() + { + // DropdownField is not supported in 2019... + var imgui = new IMGUIContainer(Localization.ShowLanguageUI); + Add(imgui); + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/LanguageSwitcherElement.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/LanguageSwitcherElement.cs.meta new file mode 100644 index 00000000..366baa08 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/LanguageSwitcherElement.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 64e0386c532046ebb565034edca90efd +timeCreated: 1693731394 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/LogoElement.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/LogoElement.cs new file mode 100644 index 00000000..3c470ac3 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/LogoElement.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.TestTools; +using UnityEngine.UIElements; + +namespace nadena.dev.modular_avatar.core.editor +{ + public class LogoElement : VisualElement + { + private const string LISTENER_REGISTERED = "ma--logo-listener-registered"; + private static WeakHashSet _activeLogos = new WeakHashSet(); + + private static Dictionary _logoDisplayNode + = null; + + private VisualElement _inner; + + private static void RegisterNode(LogoElement target) + { + if (_logoDisplayNode == null) + { + _logoDisplayNode = new Dictionary(); + EditorApplication.delayCall += () => { _logoDisplayNode = null; }; + } + + // [editor list] -> EditorElement (private) -> InspectorElement -> MAVisualElement -> Logo + VisualElement container = target; + while (container.parent != null && !(container is InspectorElement)) + { + container = container.parent; + } + + container = container.parent ?? container; // EditorElement + container = container.parent ?? container; // editor list + + if (container.ClassListContains(LISTENER_REGISTERED)) return; + container.RegisterCallback(geom => { UpdateLogoDisplayNode(container); }); + } + + private static void UpdateLogoDisplayNode(VisualElement root) + { + // Now walk down to find the LogoElements. We only walk one level past an InspectorElement (and once into + // its child MAVisualElement) to avoid descending too deep into madness. + List elements = new List(); + + WalkTree(root); + + var target = elements.FirstOrDefault(e => e.resolvedStyle.visibility == Visibility.Visible); + foreach (var elem in elements) + { + elem.LogoShown = (elem == target); + } + + void WalkTree(VisualElement visualElement) + { + if (visualElement.resolvedStyle.visibility == Visibility.Hidden || + visualElement.resolvedStyle.height < 0.5) return; + + var isInspector = visualElement.GetType() == typeof(InspectorElement); + + foreach (var child in visualElement.Children()) + { + if (child is MAVisualElement maChild) + { + foreach (var node in child.Children()) + { + if (node is LogoElement logo) + { + elements.Add(logo); + } + } + + return; + } + else if (!isInspector) + { + WalkTree(child); + } + } + } + } + + public LogoElement() + { + _inner = new VisualElement(); + + _inner.style.display = DisplayStyle.None; + _inner.style.flexDirection = FlexDirection.Row; + _inner.style.alignItems = Align.Center; + _inner.style.justifyContent = Justify.Center; + + var image = new Image(); + image.image = LogoDisplay.LOGO_ASSET; + image.style.width = new Length(LogoDisplay.ImageWidth(LogoDisplay.TARGET_HEIGHT), LengthUnit.Pixel); + image.style.height = new Length(LogoDisplay.TARGET_HEIGHT, LengthUnit.Pixel); + + _inner.Add(image); + this.Add(_inner); + + RegisterCallback(OnGeomChanged); + } + + private void OnGeomChanged(GeometryChangedEvent evt) + { + // We should be in the visual tree now + if (parent == null) return; + + RegisterNode(this); + + UnregisterCallback(OnGeomChanged); + } + + private bool _logoShown; + + private bool LogoShown + { + get => _logoShown; + set + { + if (value == _logoShown) return; + _logoShown = value; + + _inner.style.display = value ? DisplayStyle.Flex : DisplayStyle.None; + } + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/LogoElement.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/LogoElement.cs.meta new file mode 100644 index 00000000..20e0da3d --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/LogoElement.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 09c8e1f754e341deb85df120c45109d9 +timeCreated: 1693723249 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/UXMLExtensions.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/UXMLExtensions.cs new file mode 100644 index 00000000..c9d968b1 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/UXMLExtensions.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using nadena.dev.modular_avatar.core.editor; +using UnityEditor; +using UnityEngine.UIElements; + +namespace nadena.dev.modular_avatar.core.editor +{ + internal static class UXMLExtensions + { + private static Dictionary> _localizers = + new Dictionary>(); + + public static VisualElement Localize(this VisualTreeAsset asset) + { + var root = asset.CloneTree(); + + WalkTree(root); + + return root; + } + + private static void WalkTree(VisualElement elem) + { + var ty = elem.GetType(); + + GetLocalizationOperation(ty)(elem); + + foreach (var child in elem.Children()) + { + WalkTree(child); + } + } + + private static Action GetLocalizationOperation(Type ty) + { + if (!_localizers.TryGetValue(ty, out var action)) + { + PropertyInfo m_label; + if (ty == typeof(Label)) + { + m_label = ty.GetProperty("text"); + } + else + { + m_label = ty.GetProperty("label"); + } + + if (m_label == null) + { + action = _elem => { }; + } + else + { + action = elem => + { + var cur_label = m_label.GetValue(elem) as string; + if (cur_label != null && cur_label.StartsWith("##")) + { + var key = cur_label.Substring(2); + + var new_label = Localization.S(key); + var new_tooltip = Localization.S(key + ".tooltip"); + + m_label.SetValue(elem, new_label); + elem.tooltip = new_tooltip; + } + }; + } + + _localizers[ty] = action; + } + + return action; + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/UXMLExtensions.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/UXMLExtensions.cs.meta new file mode 100644 index 00000000..39bdb2f0 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/UXMLExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: eaa4c630b2ae442f8456b83ae880ca4b +timeCreated: 1693726694 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/WeakHashSet.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/WeakHashSet.cs new file mode 100644 index 00000000..d70a95b0 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/WeakHashSet.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace nadena.dev.modular_avatar.core.editor +{ + internal class WeakHashSet : IEnumerable where T : class + { + private Dictionary>> _refs = new Dictionary>>(); + + private int _amortCounter = 16; + + public void Add(T t) + { + WeakReference w = new WeakReference(t); + int hash = RuntimeHelpers.GetHashCode(t); + + if (!_refs.TryGetValue(hash, out var list)) + { + list = new List>(); + _refs[hash] = list; + } + + if (!list.Contains(w)) + { + list.Add(w); + if (_amortCounter-- <= 0) + { + ClearDeadReferences(); + } + } + } + + private void ClearDeadReferences() + { + throw new NotImplementedException(); + } + + public void Remove(T t) + { + WeakReference w = new WeakReference(t); + int hash = RuntimeHelpers.GetHashCode(t); + + if (_refs.TryGetValue(hash, out var list)) + { + list.RemoveAll(elem => elem.TryGetTarget(out var target) && target == t); + } + } + + public bool Contains(T t) + { + WeakReference w = new WeakReference(t); + int hash = RuntimeHelpers.GetHashCode(t); + + if (_refs.TryGetValue(hash, out var list)) + { + return list.Exists(elem => elem.TryGetTarget(out var target) && target == t); + } + + return false; + } + + + public IEnumerator GetEnumerator() + { + foreach (var list in _refs.Values) + { + foreach (var elem in list) + { + if (elem.TryGetTarget(out var target)) + { + yield return target; + } + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/WeakHashSet.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/WeakHashSet.cs.meta new file mode 100644 index 00000000..b1f6d5e0 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/Common/WeakHashSet.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2505c2e488e04a2caf3594fbb2e7bf94 +timeCreated: 1693724469 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/LogoDisplay.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/LogoDisplay.cs index 96447fed..b950232c 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Inspector/LogoDisplay.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/LogoDisplay.cs @@ -7,7 +7,7 @@ namespace nadena.dev.modular_avatar.core.editor internal static class LogoDisplay { internal static readonly Texture2D LOGO_ASSET; - private static float TARGET_HEIGHT => EditorStyles.label.lineHeight * 3; + internal static float TARGET_HEIGHT => EditorStyles.label.lineHeight * 3; internal static float ImageWidth(float height) { diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAEditorBase.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAEditorBase.cs index 0b6e2178..ead453d0 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAEditorBase.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MAEditorBase.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEditor; +using System.Reflection; using UnityEditor.UIElements; using UnityEngine.UIElements; @@ -9,73 +6,8 @@ namespace nadena.dev.modular_avatar.core.editor { // This class performs common setup for Modular Avatar editors, including ensuring that only one instance of the\ // logo is rendered per container. - internal abstract class MAEditorBase : Editor + public abstract class MAEditorBase : UnityEditor.Editor { - private static Dictionary - _logoDisplayNode = new Dictionary(); - - private static void Cleanup() - { - _logoDisplayNode.Clear(); - EditorApplication.update -= Cleanup; - } - - private static MAVisualElement GetCachedLogoDisplayNode(VisualElement start) - { - while (start?.parent != null && start.GetType() != typeof(InspectorElement)) - { - start = start.parent; - } - - // Next one up is an EditorElement, followed by the container of all Editors - var container = start?.parent?.parent; - - if (container == null) return null; - - if (_logoDisplayNode.TryGetValue(container, out var elem)) return elem; - - var node = FindLogoDisplayNode(container); - if (node != null) _logoDisplayNode[container] = node; - EditorApplication.update += Cleanup; - return node; - } - - private static MAVisualElement FindLogoDisplayNode(VisualElement container) - { - // Now walk down to find the MAVisualElements. We only walk one level past an InspectorElement to avoid - // descending too deep into madness. - List elements = new List(); - - WalkTree(container); - - return elements.FirstOrDefault(e => e.resolvedStyle.visibility == Visibility.Visible); - - void WalkTree(VisualElement visualElement) - { - if (visualElement.resolvedStyle.visibility == Visibility.Hidden || - visualElement.resolvedStyle.height < 0.5) return; - - var isInspector = visualElement.GetType() == typeof(InspectorElement); - - foreach (var child in visualElement.Children()) - { - if (child is MAVisualElement maChild) - { - elements.Add(maChild); - return; - } - else if (!isInspector) - { - WalkTree(child); - } - } - } - } - - private class MAVisualElement : VisualElement - { - } - private MAVisualElement _visualElement; private bool _suppressOnceDefaultMargins; @@ -84,8 +16,25 @@ namespace nadena.dev.modular_avatar.core.editor return null; } + private void RebuildUI() + { + CreateInspectorGUI(); + } + public sealed override VisualElement CreateInspectorGUI() { + if (_visualElement == null) + { + _visualElement = new MAVisualElement(); + Localization.OnLangChange += RebuildUI; + } + else + { + _visualElement.Clear(); + } + + _visualElement.Add(new LogoElement()); + // CreateInspectorElementFromEditor does a bunch of extra setup that makes our inspector look a little bit // nicer. In particular, the label column won't auto-size if we just use IMGUIElement, for some reason @@ -100,7 +49,6 @@ namespace nadena.dev.modular_avatar.core.editor inner = m.Invoke(throwaway, new object[] {serializedObject, this, false}) as VisualElement; } - _visualElement = new MAVisualElement(); _visualElement.Add(inner); _suppressOnceDefaultMargins = innerIsImgui; @@ -116,16 +64,20 @@ namespace nadena.dev.modular_avatar.core.editor public sealed override void OnInspectorGUI() { - if (GetCachedLogoDisplayNode(_visualElement) == _visualElement) - { - LogoDisplay.DisplayLogo(); - } - InspectorCommon.DisplayOutOfAvatarWarning(targets); OnInnerInspectorGUI(); } protected abstract void OnInnerInspectorGUI(); + + protected virtual void OnDestroy() + { + Localization.OnLangChange -= RebuildUI; + } + } + + internal class MAVisualElement : VisualElement + { } } \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently.meta new file mode 100644 index 00000000..f3ee0ca3 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b85e8d1dabe3455aa0b7e710f0934a2e +timeCreated: 1693722009 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependently.uxml b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependently.uxml new file mode 100644 index 00000000..2a6203ae --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependently.uxml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependently.uxml.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependently.uxml.meta new file mode 100644 index 00000000..d4ba4fb3 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependently.uxml.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 445447dbd5f94f9ca0e6a73b4744b412 +timeCreated: 1693727339 \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependentlyEditor.cs b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependentlyEditor.cs new file mode 100644 index 00000000..5dc50b47 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependentlyEditor.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using nadena.dev.modular_avatar.core.ArmatureAwase; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace nadena.dev.modular_avatar.core.editor +{ + [CustomEditor(typeof(MAMoveIndependently))] + internal class MoveIndependentlyEditor : MAEditorBase + { + [SerializeField] private StyleSheet uss; + [SerializeField] private VisualTreeAsset uxml; + + private TransformChildrenNode _groupedNodesElem; + + protected override void OnInnerInspectorGUI() + { + throw new System.NotImplementedException(); + } + + protected override VisualElement CreateInnerInspectorGUI() + { + var root = uxml.Localize(); + root.styleSheets.Add(uss); + + var container = root.Q("group-container"); + + MAMoveIndependently target = (MAMoveIndependently) this.target; + var grouped = (target.GroupedBones ?? Array.Empty()) + .Select(obj => obj.transform) + .ToImmutableHashSet(); + + _groupedNodesElem = new TransformChildrenNode(target.transform, grouped); + _groupedNodesElem.AddToClassList("group-root"); + container.Add(_groupedNodesElem); + _groupedNodesElem.OnChanged += () => + { + Undo.RecordObject(target, "Toggle grouped nodes"); + target.GroupedBones = _groupedNodesElem.Active().Select(t => t.gameObject).ToArray(); + PrefabUtility.RecordPrefabInstancePropertyModifications(target); + }; + + return root; + } + + private class TransformChildrenNode : VisualElement + { + private readonly Transform _transform; + private HashSet _active = new HashSet(); + + public Transform Transform => _transform; + + public event Action OnChanged; + + public IEnumerable Active() + { + foreach (var child in _active) + { + yield return child.Transform; + foreach (var subChild in child.Active()) + { + yield return subChild; + } + } + } + + internal TransformChildrenNode(Transform transform, ICollection enabled) + { + _transform = transform; + + foreach (Transform child in transform) + { + var childRoot = new VisualElement(); + Add(childRoot); + + var toggleContainer = new VisualElement(); + childRoot.Add(toggleContainer); + toggleContainer.AddToClassList("left-toggle"); + var toggle = new Toggle(); + toggleContainer.Add(toggle); + toggleContainer.Add(new Label(child.gameObject.name)); + + var childGroup = new VisualElement(); + childRoot.Add(toggleContainer); + childRoot.Add(childGroup); + + childGroup.AddToClassList("group-children"); + + TransformChildrenNode childNode = null; + Action setNodeState = newValue => + { + if (childNode != null == newValue) return; + + if (newValue) + { + childNode = new TransformChildrenNode(child, enabled); + _active.Add(childNode); + childNode.OnChanged += FireOnChanged; + childGroup.Add(childNode); + } + else + { + childGroup.Clear(); + _active.Remove(childNode); + childNode = null; + } + + FireOnChanged(); + }; + + toggle.RegisterValueChangedCallback(ev => setNodeState(ev.newValue)); + toggle.value = enabled.Contains(child); + setNodeState(toggle.value); + } + + enabled = ImmutableHashSet.Empty; + } + + private void FireOnChanged() + { + OnChanged?.Invoke(); + } + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependentlyEditor.cs.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependentlyEditor.cs.meta new file mode 100644 index 00000000..61465cab --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependentlyEditor.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 1a6b092858f949e8a4540f73244f6d30 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - uss: {fileID: 7433441132597879392, guid: f6acca9e5ad360048ae15c13abe12644, type: 3} + - uxml: {fileID: 9197481963319205126, guid: 445447dbd5f94f9ca0e6a73b4744b412, type: 3} + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependentlyStyles.uss b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependentlyStyles.uss new file mode 100644 index 00000000..9fa16ca7 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependentlyStyles.uss @@ -0,0 +1,36 @@ +VisualElement { +} + +#group-box { + margin-top: 4px; + margin-bottom: 4px; + padding: 4px; + border-width: 3px; + border-left-color: rgba(0, 1, 0, 0.2); + border-top-color: rgba(0, 1, 0, 0.2); + border-right-color: rgba(0, 1, 0, 0.2); + border-bottom-color: rgba(0, 1, 0, 0.2); + border-radius: 4px; + /* background-color: rgba(0, 0, 0, 0.1); */ +} + +#group-box > Label { + -unity-font-style: bold; +} + +.group-root { + margin-top: 4px; +} + +.group-root Toggle { + margin-left: 0; +} + +.group-children { + padding-left: 10px; +} + +.left-toggle { + display: flex; + flex-direction: row; +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependentlyStyles.uss.meta b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependentlyStyles.uss.meta new file mode 100644 index 00000000..6b9e5eb2 --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Editor/Inspector/MoveIndependently/MoveIndependentlyStyles.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6acca9e5ad360048ae15c13abe12644 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/Packages/nadena.dev.modular-avatar/Editor/Localization/en.json b/Packages/nadena.dev.modular-avatar/Editor/Localization/en.json index 85ccb2a1..63ac28c1 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Localization/en.json +++ b/Packages/nadena.dev.modular-avatar/Editor/Localization/en.json @@ -162,5 +162,6 @@ "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:" + "setup_outfit.err.no_outfit_hips": "Unable to identify the Hips object for the outfit. Searched for objects containing the following names:", + "move_independently.group-header": "Objects to move together" } \ 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 55898077..d290a9e9 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Localization/ja.json +++ b/Packages/nadena.dev.modular-avatar/Editor/Localization/ja.json @@ -160,5 +160,6 @@ "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ボーンを発見できませんでした。以下の名前を含むボーンを探しました:" + "setup_outfit.err.no_outfit_hips": "衣装のHipsボーンを発見できませんでした。以下の名前を含むボーンを探しました:", + "move_independently.group-header": "一緒に動かすオブジェクト" } diff --git a/Packages/nadena.dev.modular-avatar/Runtime/MAMoveIndependently.cs b/Packages/nadena.dev.modular-avatar/Runtime/MAMoveIndependently.cs new file mode 100644 index 00000000..003c794e --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Runtime/MAMoveIndependently.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using VRC.SDKBase; + +namespace nadena.dev.modular_avatar.core.ArmatureAwase +{ + [ExecuteInEditMode] + //[AddComponentMenu("")] + [DisallowMultipleComponent] + class MAMoveIndependently : MonoBehaviour, IEditorOnly + { + private float EPSILON = 0.000001f; + + private GameObject[] m_groupedBones; + + public GameObject[] GroupedBones + { + get => m_groupedBones.Clone() as GameObject[]; + set + { + m_groupedBones = value.Clone() as GameObject[]; + OnValidate(); + } + } + + private Matrix4x4 _priorFrameState; + + struct ChildState + { + internal Vector3 childLocalPos; + internal Quaternion childLocalRot; + internal Vector3 childLocalScale; + + // The child world position, recorded when we first initialized (or after unexpected child movement) + internal Matrix4x4 childWorld; + } + + private Dictionary _children = new Dictionary(); + private HashSet _excluded = new HashSet(); + + void Awake() + { + hideFlags = HideFlags.DontSave; + } + + // We need to reparent the TRS values of the children from our prior frame state to the current frame state. + // This is done by computing the world affine matrix for the child in the prior frame, then converting to + // a local affine matrix in the current frame. + + private void OnValidate() + { + Debug.Log("=== OnValidate"); + hideFlags = HideFlags.DontSave; + _excluded = new HashSet(); + if (m_groupedBones == null) + { + m_groupedBones = Array.Empty(); + } + + foreach (var grouped in m_groupedBones) + { + if (grouped != null) + { + _excluded.Add(grouped.transform); + } + } + + _priorFrameState = transform.localToWorldMatrix; + _children.Clear(); + CheckChildren(); + } + + HashSet _observed = new HashSet(); + + private void CheckChildren() + { + _observed.Clear(); + + CheckChildren(transform); + foreach (var obj in m_groupedBones) + { + CheckChildren(obj.transform); + } + + // Remove any children that are no longer children + var toRemove = new List(); + foreach (var child in _children) + { + if (child.Key == null || !_observed.Contains(child.Key)) + { + toRemove.Add(child.Key); + } + } + + foreach (var child in toRemove) + { + _children.Remove(child); + } + } + + private void CheckChildren(Transform parent) + { + foreach (Transform child in parent) + { + if (_excluded.Contains(child)) continue; + + _observed.Add(child); + + var localPosition = child.localPosition; + var localRotation = child.localRotation; + var localScale = child.localScale; + + if (_children.TryGetValue(child, out var state)) + { + var deltaPos = localPosition - state.childLocalPos; + var deltaRot = Quaternion.Angle(localRotation, state.childLocalRot); + var deltaScale = (localScale - state.childLocalScale).sqrMagnitude; + + if (deltaPos.sqrMagnitude < EPSILON && deltaRot < EPSILON && deltaScale < EPSILON) + { + Matrix4x4 childNewLocal = parent.worldToLocalMatrix * state.childWorld; + + Undo.RecordObject(child, Undo.GetCurrentGroupName()); + + child.localPosition = childNewLocal.MultiplyPoint(Vector3.zero); + child.localRotation = childNewLocal.rotation; + child.localScale = childNewLocal.lossyScale; + + state.childLocalPos = child.localPosition; + state.childLocalRot = child.localRotation; + state.childLocalScale = child.localScale; + + _children[child] = state; + + continue; + } + } + + Matrix4x4 childTRS = Matrix4x4.TRS(localPosition, localRotation, localScale); + + state = new ChildState() + { + childLocalPos = localPosition, + childLocalRot = localRotation, + childLocalScale = localScale, + childWorld = parent.localToWorldMatrix * childTRS, + }; + + _children[child] = state; + } + } + + void Update() + { + var deltaPos = transform.position - _priorFrameState.MultiplyPoint(Vector3.zero); + var deltaRot = Quaternion.Angle(_priorFrameState.rotation, transform.rotation); + var deltaScale = (transform.lossyScale - _priorFrameState.lossyScale).sqrMagnitude; + + if (deltaPos.sqrMagnitude < EPSILON && deltaRot < EPSILON && deltaScale < EPSILON) return; + + CheckChildren(); + } + } +} \ No newline at end of file diff --git a/Packages/nadena.dev.modular-avatar/Runtime/MAMoveIndependently.cs.meta b/Packages/nadena.dev.modular-avatar/Runtime/MAMoveIndependently.cs.meta new file mode 100644 index 00000000..b175c9bc --- /dev/null +++ b/Packages/nadena.dev.modular-avatar/Runtime/MAMoveIndependently.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a8d5b07828ba4eefb9acc305478369d0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: a8edd5bd1a0a64a40aa99cc09fb5f198, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index de9852ec..bb0a5bea 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -187,6 +187,12 @@ "source": "embedded", "dependencies": {} }, + "nadena.dev.av3-build-framework": { + "version": "file:nadena.dev.av3-build-framework", + "depth": 0, + "source": "embedded", + "dependencies": {} + }, "nadena.dev.modular-avatar": { "version": "file:nadena.dev.modular-avatar", "depth": 0, diff --git a/UIElementsSchema/UIElements.xsd b/UIElementsSchema/UIElements.xsd new file mode 100644 index 00000000..12afeab3 --- /dev/null +++ b/UIElementsSchema/UIElements.xsd @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/UIElementsSchema/UnityEditor.PackageManager.UI.xsd b/UIElementsSchema/UnityEditor.PackageManager.UI.xsd new file mode 100644 index 00000000..c7cc9001 --- /dev/null +++ b/UIElementsSchema/UnityEditor.PackageManager.UI.xsd @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UIElementsSchema/UnityEditor.UIElements.Debugger.xsd b/UIElementsSchema/UnityEditor.UIElements.Debugger.xsd new file mode 100644 index 00000000..27c2132c --- /dev/null +++ b/UIElementsSchema/UnityEditor.UIElements.Debugger.xsd @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UIElementsSchema/UnityEditor.UIElements.xsd b/UIElementsSchema/UnityEditor.UIElements.xsd new file mode 100644 index 00000000..4d59d30c --- /dev/null +++ b/UIElementsSchema/UnityEditor.UIElements.xsd @@ -0,0 +1,887 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UIElementsSchema/UnityEngine.UIElements.xsd b/UIElementsSchema/UnityEngine.UIElements.xsd new file mode 100644 index 00000000..ac366ff1 --- /dev/null +++ b/UIElementsSchema/UnityEngine.UIElements.xsd @@ -0,0 +1,525 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UIElementsSchema/nadena.dev.modular_avatar.core.editor.xsd b/UIElementsSchema/nadena.dev.modular_avatar.core.editor.xsd new file mode 100644 index 00000000..e0d3bb51 --- /dev/null +++ b/UIElementsSchema/nadena.dev.modular_avatar.core.editor.xsd @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/docs/docs/reference/move-independently.md b/docs/docs/reference/move-independently.md new file mode 100644 index 00000000..f61bc68b --- /dev/null +++ b/docs/docs/reference/move-independently.md @@ -0,0 +1,23 @@ +import ReactPlayer from 'react-player' + +# Move Independently + + + +The Move Independently component allows you to move an object without affecting its children. +This component has no effect at runtime; it is purely for use in the editor. + +## When should I use it? + +This component is intended to be used when adjusting the fit of outfits on your avatar. You can, for example, +adjust the position of the hips object of the outfit without impacting the position of other objects. + +## Grouping objects + +By checking boxes under the "Objects to move together" field, you can create a group of objects that move together. +For example, you might move the hips and upper leg objects together, but leave the lower leg objects behind. + +## Limitations + +While this component supports scaling an object independently of its children, non-uniform scales (where the X, Y, and Z +scales are not all the same) are not fully supported, and may result in unexpected behavior. diff --git a/docs/i18n/ja/docusaurus-plugin-content-docs/current/reference/move-independently.md b/docs/i18n/ja/docusaurus-plugin-content-docs/current/reference/move-independently.md new file mode 100644 index 00000000..0bc99f0d --- /dev/null +++ b/docs/i18n/ja/docusaurus-plugin-content-docs/current/reference/move-independently.md @@ -0,0 +1,23 @@ +import ReactPlayer from 'react-player' + +# Move Independently + + + +MA Move Independentlyというコンポーネントを使うと、子オブジェクトに影響を与えずにオブジェクトを移動させることができます。 +このコンポーネントはランタイムでは何の効果もありません。エディターでのみ使用できます。 + +## いつ使うべきか + +このコンポーネントは、アバターに衣装のフィットを調整する際に使用することを想定しています。例えば、 +衣装のヒップオブジェクトの位置を調整しつつ、他のオブジェクトの位置に影響しないようにできます。 + +## オブジェクトをグループ化する + +「一緒に動かすオブジェクト」欄のチェックボックスをオンにすることで、一緒に移動するオブジェクトのグループを作成できます。 +例えば、HipsとUpper Legのオブジェクトを一緒に移動させ、Lower Legのオブジェクトをそのままにすることができます。 + +## 制限事項 + +子に影響を与えずにオブジェクトの大きさを調整することは対応していますが、XYZそれぞれのスケールが同じ出ない場合は +対応されず、変な挙動になることがあります。ご注意ください。 \ No newline at end of file diff --git a/docs/static/img/move-independently.mp4 b/docs/static/img/move-independently.mp4 new file mode 100644 index 00000000..32026382 Binary files /dev/null and b/docs/static/img/move-independently.mp4 differ