diff --git a/.github/ProjectRoot/vpm-manifest-2022.json b/.github/ProjectRoot/vpm-manifest-2022.json index 64c967e7..d4a80444 100644 --- a/.github/ProjectRoot/vpm-manifest-2022.json +++ b/.github/ProjectRoot/vpm-manifest-2022.json @@ -19,7 +19,7 @@ "dependencies": {} }, "nadena.dev.ndmf": { - "version": "1.5.0-rc.3" + "version": "1.5.0-rc.11" } } } \ No newline at end of file diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index d58efa8a..82bb1ed1 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -122,7 +122,7 @@ jobs: workingDirectory: docs-site~ - name: Purge cache - uses: nathanvaughn/actions-cloudflare-purge@367672c723960cd03bb7d8c2c4d89062a3fc1fac + uses: nathanvaughn/actions-cloudflare-purge@aa1121a867565ea71b60f445f441544df0c7b0b9 continue-on-error: true with: cf_zone: ${{ secrets.CF_ZONE_ID }} diff --git a/Editor/Animation/AnimationDatabase.cs b/Editor/Animation/AnimationDatabase.cs index 0d2e97c7..fd9d8552 100644 --- a/Editor/Animation/AnimationDatabase.cs +++ b/Editor/Animation/AnimationDatabase.cs @@ -9,6 +9,7 @@ using nadena.dev.ndmf; using UnityEditor; using UnityEditor.Animations; using UnityEngine; +using UnityEngine.Profiling; using BuildContext = nadena.dev.ndmf.BuildContext; #if MA_VRCSDK3_AVATARS using VRC.SDK3.Avatars.Components; @@ -97,11 +98,13 @@ namespace nadena.dev.modular_avatar.animation internal void Commit() { + Profiler.BeginSample("AnimationDatabase.Commit"); foreach (var clip in _clips) { if (clip.IsProxyAnimation) clip.CurrentClip = clip.OriginalClip; } + Profiler.BeginSample("UpdateClipProperties"); foreach (var clip in _clips) { // Changing the "high quality curve" setting can result in behavior changes (but can happen accidentally @@ -121,11 +124,16 @@ namespace nadena.dev.modular_avatar.animation } } } + Profiler.EndSample(); + Profiler.BeginSample("ClipCommitActions"); foreach (var action in _clipCommitActions) { action(); } + Profiler.EndSample(); + + Profiler.EndSample(); } internal void OnActivate(BuildContext context) @@ -194,7 +202,11 @@ namespace nadena.dev.modular_avatar.animation var clipHolder = RegisterMotion(state.motion, state, processClip, _originalToHolder); state.motion = clipHolder.CurrentClip; - _clipCommitActions.Add(() => { state.motion = clipHolder.CurrentClip; }); + _clipCommitActions.Add(() => + { + state.motion = clipHolder.CurrentClip; + MaybeSaveClip(clipHolder.CurrentClip); + }); } internal void ForeachClip(Action processClip) @@ -370,6 +382,8 @@ namespace nadena.dev.modular_avatar.animation children[i].motion = curClip; dirty = true; } + + MaybeSaveClip(curClip); } if (dirty) @@ -381,5 +395,23 @@ namespace nadena.dev.modular_avatar.animation return treeHolder; } + + private void MaybeSaveClip(Motion curClip) + { + Profiler.BeginSample("MaybeSaveClip"); + if (curClip != null && !EditorUtility.IsPersistent(curClip) && EditorUtility.IsPersistent(_context.AssetContainer) && _context.AssetContainer != null) + { + try + { + AssetDatabase.AddObjectToAsset(curClip, _context.AssetContainer); + } + catch (Exception e) + { + Debug.LogException(e); + throw; + } + } + Profiler.EndSample(); + } } } \ No newline at end of file diff --git a/Editor/Animation/AnimationUtil.cs b/Editor/Animation/AnimationUtil.cs index 976e6122..2c138544 100644 --- a/Editor/Animation/AnimationUtil.cs +++ b/Editor/Animation/AnimationUtil.cs @@ -42,7 +42,9 @@ namespace nadena.dev.modular_avatar.animation throw new Exception("Unknown RuntimeAnimatorContoller type " + controller.GetType()); } - return merger.Finish(); + var clone = merger.Finish(); + ObjectRegistry.RegisterReplacedObject(controller, clone); + return clone; } internal static void CloneAllControllers(BuildContext context) diff --git a/Editor/Animation/AnimatorCombiner.cs b/Editor/Animation/AnimatorCombiner.cs index 4fa96102..36ddf4de 100644 --- a/Editor/Animation/AnimatorCombiner.cs +++ b/Editor/Animation/AnimatorCombiner.cs @@ -473,7 +473,7 @@ namespace nadena.dev.modular_avatar.animation var newLayer = new AnimatorControllerLayer() { name = layer.name, - avatarMask = layer.avatarMask, // TODO map transforms + avatarMask = _deepClone.DoClone(layer.avatarMask, basePath, _cloneMap), blendingMode = layer.blendingMode, defaultWeight = first ? 1 : layer.defaultWeight, syncedLayerIndex = layer.syncedLayerIndex, @@ -494,7 +494,8 @@ namespace nadena.dev.modular_avatar.animation var overrideMotion = layer.GetOverrideMotion(state); if (overrideMotion != null) { - newLayer.SetOverrideMotion((AnimatorState)_cloneMap[state], overrideMotion); + var newMotion = _deepClone.DoClone(overrideMotion, basePath, _cloneMap); + newLayer.SetOverrideMotion((AnimatorState)_cloneMap[state], newMotion); } var overrideBehaviors = (StateMachineBehaviour[])layer.GetOverrideBehaviours(state)?.Clone(); diff --git a/Editor/Animation/DeepClone.cs b/Editor/Animation/DeepClone.cs index 8980548e..e84bac6f 100644 --- a/Editor/Animation/DeepClone.cs +++ b/Editor/Animation/DeepClone.cs @@ -50,6 +50,7 @@ namespace nadena.dev.modular_avatar.animation case AnimatorStateMachine _: case AnimatorTransitionBase _: case StateMachineBehaviour _: + case AvatarMask _: break; // We want to clone these types case AudioClip _: //Used in VRC Animator Play Audio State Behavior @@ -92,10 +93,17 @@ namespace nadena.dev.modular_avatar.animation { ObjectRegistry.RegisterReplacedObject(original, obj); } + + if (_isSaved && !EditorUtility.IsPersistent(obj)) + { + AssetDatabase.AddObjectToAsset(obj, _combined); + } return (T)obj; } + + var ctor = original.GetType().GetConstructor(Type.EmptyTypes); if (ctor == null || original is ScriptableObject) { @@ -146,8 +154,74 @@ namespace nadena.dev.modular_avatar.animation return (T)obj; } + // internal for testing + internal static AvatarMask CloneAvatarMask(AvatarMask mask, string basePath) + { + if (basePath.EndsWith("/")) basePath = basePath.Substring(0, basePath.Length - 1); + + var newMask = new AvatarMask(); + + // Transfer first the humanoid mask data + EditorUtility.CopySerialized(mask, newMask); + + var srcSo = new SerializedObject(mask); + var dstSo = new SerializedObject(newMask); + var srcElements = srcSo.FindProperty("m_Elements"); + + if (basePath == "" || srcElements.arraySize == 0) return newMask; // no changes required + + // We now need to prefix the elements of basePath (with weight zero) + + var newElements = new List(); + + var accum = ""; + foreach (var element in basePath.Split("/")) + { + if (accum != "") accum += "/"; + accum += element; + + newElements.Add(accum); + } + + var dstElements = dstSo.FindProperty("m_Elements"); + + // We'll need to create new array elements by using DuplicateCommand. We'll then rewrite the whole + // list to keep things in traversal order. + for (var i = 0; i < newElements.Count; i++) dstElements.GetArrayElementAtIndex(0).DuplicateCommand(); + + var totalElements = srcElements.arraySize + newElements.Count; + for (var i = 0; i < totalElements; i++) + { + var dstElem = dstElements.GetArrayElementAtIndex(i); + var dstPath = dstElem.FindPropertyRelative("m_Path"); + var dstWeight = dstElem.FindPropertyRelative("m_Weight"); + + var srcIndex = i - newElements.Count; + if (srcIndex < 0) + { + dstPath.stringValue = newElements[i]; + dstWeight.floatValue = 0; + } + else + { + var srcElem = srcElements.GetArrayElementAtIndex(srcIndex); + dstPath.stringValue = basePath + "/" + srcElem.FindPropertyRelative("m_Path").stringValue; + dstWeight.floatValue = srcElem.FindPropertyRelative("m_Weight").floatValue; + } + } + + dstSo.ApplyModifiedPropertiesWithoutUndo(); + + return newMask; + } + private UnityObject CloneWithPathMapping(UnityObject o, string basePath) { + if (o is AvatarMask mask) + { + return CloneAvatarMask(mask, basePath); + } + if (o is AnimationClip clip) { // We'll always rebase if the asset is non-persistent, because we can't reference a nonpersistent asset diff --git a/Editor/Animation/PathMappings.cs b/Editor/Animation/PathMappings.cs index a830ebfe..9546a063 100644 --- a/Editor/Animation/PathMappings.cs +++ b/Editor/Animation/PathMappings.cs @@ -6,7 +6,9 @@ using System.Linq; using nadena.dev.ndmf; using nadena.dev.ndmf.util; using UnityEditor; +using UnityEditor.Animations; using UnityEngine; +using UnityEngine.Profiling; #if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER #endif @@ -278,10 +280,68 @@ namespace nadena.dev.modular_avatar.animation return newClip; } + private void ApplyMappingsToAvatarMask(AvatarMask mask) + { + if (mask == null) return; + + var maskSo = new SerializedObject(mask); + + var seenTransforms = new Dictionary(); + var transformOrder = new List(); + var m_Elements = maskSo.FindProperty("m_Elements"); + var elementCount = m_Elements.arraySize; + + for (var i = 0; i < elementCount; i++) + { + var element = m_Elements.GetArrayElementAtIndex(i); + var path = element.FindPropertyRelative("m_Path").stringValue; + var weight = element.FindPropertyRelative("m_Weight").floatValue; + + path = MapPath(path); + + // ensure all parent elements are present + EnsureParentsPresent(path); + + if (!seenTransforms.ContainsKey(path)) transformOrder.Add(path); + seenTransforms[path] = weight; + } + + transformOrder.Sort(); + m_Elements.arraySize = transformOrder.Count; + + for (var i = 0; i < transformOrder.Count; i++) + { + var element = m_Elements.GetArrayElementAtIndex(i); + var path = transformOrder[i]; + + element.FindPropertyRelative("m_Path").stringValue = path; + element.FindPropertyRelative("m_Weight").floatValue = seenTransforms[path]; + } + + maskSo.ApplyModifiedPropertiesWithoutUndo(); + + void EnsureParentsPresent(string path) + { + var nextSlash = -1; + + while ((nextSlash = path.IndexOf('/', nextSlash + 1)) != -1) + { + var parentPath = path.Substring(0, nextSlash); + if (!seenTransforms.ContainsKey(parentPath)) + { + seenTransforms[parentPath] = 0; + transformOrder.Add(parentPath); + } + } + } + } + internal void OnDeactivate(BuildContext context) { + Profiler.BeginSample("PathMappings.OnDeactivate"); Dictionary clipCache = new Dictionary(); - + + Profiler.BeginSample("ApplyMappingsToClip"); _animationDatabase.ForeachClip(holder => { if (holder.CurrentClip is AnimationClip clip) @@ -289,19 +349,42 @@ namespace nadena.dev.modular_avatar.animation holder.CurrentClip = ApplyMappingsToClip(clip, clipCache); } }); + Profiler.EndSample(); #if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER + Profiler.BeginSample("MapPlayAudio"); _animationDatabase.ForeachPlayAudio(playAudio => { if (playAudio == null) return; playAudio.SourcePath = MapPath(playAudio.SourcePath, true); }); + Profiler.EndSample(); #endif + Profiler.BeginSample("InvokeIOnCommitObjectRenamesCallbacks"); foreach (var listener in context.AvatarRootObject.GetComponentsInChildren()) { listener.OnCommitObjectRenames(context, this); } + Profiler.EndSample(); + + var layers = context.AvatarDescriptor.baseAnimationLayers + .Concat(context.AvatarDescriptor.specialAnimationLayers); + + Profiler.BeginSample("ApplyMappingsToAvatarMasks"); + foreach (var layer in layers) + { + ApplyMappingsToAvatarMask(layer.mask); + + if (layer.animatorController is AnimatorController ac) + // By this point, all AnimationOverrideControllers have been collapsed into an ephemeral + // AnimatorController so we can safely modify the controller in-place. + foreach (var acLayer in ac.layers) + ApplyMappingsToAvatarMask(acLayer.avatarMask); + } + Profiler.EndSample(); + + Profiler.EndSample(); } public GameObject PathToObject(string path) diff --git a/Editor/ApplyAnimatorDefaultValuesPass.cs b/Editor/ApplyAnimatorDefaultValuesPass.cs index f2554d13..7b3c91ed 100644 --- a/Editor/ApplyAnimatorDefaultValuesPass.cs +++ b/Editor/ApplyAnimatorDefaultValuesPass.cs @@ -40,7 +40,7 @@ namespace nadena.dev.modular_avatar.core.editor switch (parameters[i].type) { case AnimatorControllerParameterType.Bool: - parameters[i].defaultBool = defaultValue > 0.5f; + parameters[i].defaultBool = defaultValue != 0.0f; break; case AnimatorControllerParameterType.Int: parameters[i].defaultInt = Mathf.RoundToInt(defaultValue); diff --git a/Editor/HeuristicBoneMapper.cs b/Editor/HeuristicBoneMapper.cs index 21ce9381..b1ba169b 100644 --- a/Editor/HeuristicBoneMapper.cs +++ b/Editor/HeuristicBoneMapper.cs @@ -36,17 +36,17 @@ namespace nadena.dev.modular_avatar.core.editor }, new[] { - "LeftLowerLeg", "LowerLeg_Left", "LowerLeg_L", "Knee_Left", "Knee_L", "LLeg_L", "Left knee", "LeftLeg", "leg_L" + "LeftLowerLeg", "LowerLeg_Left", "LowerLeg_L", "Knee_Left", "Knee_L", "LLeg_L", "Left knee", "LeftLeg", "leg_L", "shin.L" }, new[] { "RightLowerLeg", "LowerLeg_Right", "LowerLeg_R", "Knee_Right", "Knee_R", "LLeg_R", "Right knee", - "RightLeg", "leg_R" + "RightLeg", "leg_R", "shin.R" }, new[] {"LeftFoot", "Foot_Left", "Foot_L", "Ankle_L", "Foot.L.001", "Left ankle", "heel.L", "heel"}, new[] {"RightFoot", "Foot_Right", "Foot_R", "Ankle_R", "Foot.R.001", "Right ankle", "heel.R", "heel"}, new[] {"Spine", "spine01"}, - new[] {"Chest", "Bust", "spine02"}, + new[] {"Chest", "Bust", "spine02", "upper_chest"}, new[] {"Neck"}, new[] {"Head"}, new[] {"LeftShoulder", "Shoulder_Left", "Shoulder_L"}, @@ -95,62 +95,62 @@ namespace nadena.dev.modular_avatar.core.editor new[] { "LeftIndexProximal", "ProximalIndex_Left", "ProximalIndex_L", "Index1_L", "IndexFinger1_L", - "LeftHandIndex1", "Index Proximal.L", "finger02_01_L" + "LeftHandIndex1", "Index Proximal.L", "finger02_01_L", "f_index.01.L" }, new[] { "LeftIndexIntermediate", "IntermediateIndex_Left", "IntermediateIndex_L", "Index2_L", "IndexFinger2_L", - "LeftHandIndex2", "Index Intermediate.L", "finger02_02_L" + "LeftHandIndex2", "Index Intermediate.L", "finger02_02_L", "f_index.02.L" }, new[] { "LeftIndexDistal", "DistalIndex_Left", "DistalIndex_L", "Index3_L", "IndexFinger3_L", "LeftHandIndex3", - "Index Distal.L", "finger02_03_L" + "Index Distal.L", "finger02_03_L", "f_index.03.L" }, new[] { "LeftMiddleProximal", "ProximalMiddle_Left", "ProximalMiddle_L", "Middle1_L", "MiddleFinger1_L", - "LeftHandMiddle1", "Middle Proximal.L", "finger03_01_L" + "LeftHandMiddle1", "Middle Proximal.L", "finger03_01_L", "f_middle.01.L" }, new[] { "LeftMiddleIntermediate", "IntermediateMiddle_Left", "IntermediateMiddle_L", "Middle2_L", - "MiddleFinger2_L", "LeftHandMiddle2", "Middle Intermediate.L", "finger03_02_L" + "MiddleFinger2_L", "LeftHandMiddle2", "Middle Intermediate.L", "finger03_02_L", "f_middle.02.L" }, new[] { "LeftMiddleDistal", "DistalMiddle_Left", "DistalMiddle_L", "Middle3_L", "MiddleFinger3_L", - "LeftHandMiddle3", "Middle Distal.L", "finger03_03_L" + "LeftHandMiddle3", "Middle Distal.L", "finger03_03_L", "f_middle.03.L" }, new[] { "LeftRingProximal", "ProximalRing_Left", "ProximalRing_L", "Ring1_L", "RingFinger1_L", "LeftHandRing1", - "Ring Proximal.L", "finger04_01_L" + "Ring Proximal.L", "finger04_01_L", "f_ring.01.L" }, new[] { "LeftRingIntermediate", "IntermediateRing_Left", "IntermediateRing_L", "Ring2_L", "RingFinger2_L", - "LeftHandRing2", "Ring Intermediate.L", "finger04_02_L" + "LeftHandRing2", "Ring Intermediate.L", "finger04_02_L", "f_ring.02.L" }, new[] { "LeftRingDistal", "DistalRing_Left", "DistalRing_L", "Ring3_L", "RingFinger3_L", "LeftHandRing3", - "Ring Distal.L", "finger04_03_L" + "Ring Distal.L", "finger04_03_L", "f_ring.03.L" }, new[] { "LeftLittleProximal", "ProximalLittle_Left", "ProximalLittle_L", "Little1_L", "LittleFinger1_L", - "LeftHandPinky1", "Little Proximal.L", "finger05_01_L" + "LeftHandPinky1", "Little Proximal.L", "finger05_01_L", "f_pinky.01.L" }, new[] { "LeftLittleIntermediate", "IntermediateLittle_Left", "IntermediateLittle_L", "Little2_L", - "LittleFinger2_L", "LeftHandPinky2", "Little Intermediate.L", "finger05_02_L" + "LittleFinger2_L", "LeftHandPinky2", "Little Intermediate.L", "finger05_02_L", "f_pinky.02.L" }, new[] { "LeftLittleDistal", "DistalLittle_Left", "DistalLittle_L", "Little3_L", "LittleFinger3_L", - "LeftHandPinky3", "Little Distal.L", "finger05_03_L" + "LeftHandPinky3", "Little Distal.L", "finger05_03_L", "f_pinky.03.L" }, new[] { @@ -170,62 +170,62 @@ namespace nadena.dev.modular_avatar.core.editor new[] { "RightIndexProximal", "ProximalIndex_Right", "ProximalIndex_R", "Index1_R", "IndexFinger1_R", - "RightHandIndex1", "Index Proximal.R", "finger02_01_R" + "RightHandIndex1", "Index Proximal.R", "finger02_01_R", "f_index.01.R" }, new[] { "RightIndexIntermediate", "IntermediateIndex_Right", "IntermediateIndex_R", "Index2_R", - "IndexFinger2_R", "RightHandIndex2", "Index Intermediate.R", "finger02_02_R" + "IndexFinger2_R", "RightHandIndex2", "Index Intermediate.R", "finger02_02_R", "f_index.02.R" }, new[] { "RightIndexDistal", "DistalIndex_Right", "DistalIndex_R", "Index3_R", "IndexFinger3_R", - "RightHandIndex3", "Index Distal.R", "finger02_03_R" + "RightHandIndex3", "Index Distal.R", "finger02_03_R", "f_index.03.R" }, new[] { "RightMiddleProximal", "ProximalMiddle_Right", "ProximalMiddle_R", "Middle1_R", "MiddleFinger1_R", - "RightHandMiddle1", "Middle Proximal.R", "finger03_01_R" + "RightHandMiddle1", "Middle Proximal.R", "finger03_01_R", "f_middle.01.R" }, new[] { "RightMiddleIntermediate", "IntermediateMiddle_Right", "IntermediateMiddle_R", "Middle2_R", - "MiddleFinger2_R", "RightHandMiddle2", "Middle Intermediate.R", "finger03_02_R" + "MiddleFinger2_R", "RightHandMiddle2", "Middle Intermediate.R", "finger03_02_R", "f_middle.02.R" }, new[] { "RightMiddleDistal", "DistalMiddle_Right", "DistalMiddle_R", "Middle3_R", "MiddleFinger3_R", - "RightHandMiddle3", "Middle Distal.R", "finger03_03_R" + "RightHandMiddle3", "Middle Distal.R", "finger03_03_R", "f_middle.03.R" }, new[] { "RightRingProximal", "ProximalRing_Right", "ProximalRing_R", "Ring1_R", "RingFinger1_R", - "RightHandRing1", "Ring Proximal.R", "finger04_01_R" + "RightHandRing1", "Ring Proximal.R", "finger04_01_R", "f_ring.01.R" }, new[] { "RightRingIntermediate", "IntermediateRing_Right", "IntermediateRing_R", "Ring2_R", "RingFinger2_R", - "RightHandRing2", "Ring Intermediate.R", "finger04_02_R" + "RightHandRing2", "Ring Intermediate.R", "finger04_02_R", "f_ring.02.R" }, new[] { "RightRingDistal", "DistalRing_Right", "DistalRing_R", "Ring3_R", "RingFinger3_R", "RightHandRing3", - "Ring Distal.R", "finger04_03_R" + "Ring Distal.R", "finger04_03_R", "f_ring.03.R" }, new[] { "RightLittleProximal", "ProximalLittle_Right", "ProximalLittle_R", "Little1_R", "LittleFinger1_R", - "RightHandPinky1", "Little Proximal.R", "finger05_01_R" + "RightHandPinky1", "Little Proximal.R", "finger05_01_R", "f_pinky.01.R" }, new[] { "RightLittleIntermediate", "IntermediateLittle_Right", "IntermediateLittle_R", "Little2_R", - "LittleFinger2_R", "RightHandPinky2", "Little Intermediate.R", "finger05_02_R" + "LittleFinger2_R", "RightHandPinky2", "Little Intermediate.R", "finger05_02_R", "f_pinky.02.R" }, new[] { "RightLittleDistal", "DistalLittle_Right", "DistalLittle_R", "Little3_R", "LittleFinger3_R", - "RightHandPinky3", "Little Distal.R", "finger05_03_R" + "RightHandPinky3", "Little Distal.R", "finger05_03_R", "f_pinky.03.R" }, new[] {"UpperChest", "UChest"}, }; diff --git a/Editor/Inspector/BoneProxyEditor.cs b/Editor/Inspector/BoneProxyEditor.cs index f9d3ec6d..22566984 100644 --- a/Editor/Inspector/BoneProxyEditor.cs +++ b/Editor/Inspector/BoneProxyEditor.cs @@ -95,7 +95,7 @@ namespace nadena.dev.modular_avatar.core.editor var t = (ModularAvatarBoneProxy) targets[i]; Undo.RecordObjects(targets, "Set targets"); var xform = ((TempObjRef) objRefs[i]).target; - if (RuntimeUtil.FindAvatarTransformInParents(xform)?.gameObject != parentAvatar) continue; + if (xform != null && RuntimeUtil.FindAvatarTransformInParents(xform)?.gameObject != parentAvatar) continue; t.target = xform; } } @@ -159,4 +159,4 @@ namespace nadena.dev.modular_avatar.core.editor } } } -} \ No newline at end of file +} diff --git a/Editor/Inspector/FirstPersonVisibleEditor.cs b/Editor/Inspector/FirstPersonVisibleEditor.cs index 81b51ad2..91052b28 100644 --- a/Editor/Inspector/FirstPersonVisibleEditor.cs +++ b/Editor/Inspector/FirstPersonVisibleEditor.cs @@ -35,6 +35,9 @@ namespace nadena.dev.modular_avatar.core.editor case VisibleHeadAccessoryValidation.ReadyStatus.ParentMarked: EditorGUILayout.HelpBox(Localization.S("fpvisible.normal"), MessageType.Info); break; + case VisibleHeadAccessoryValidation.ReadyStatus.NotUnderHead: + EditorGUILayout.HelpBox(Localization.S("fpvisible.NotUnderHead"), MessageType.Warning); + break; default: { var label = "fpvisible." + status; @@ -49,4 +52,4 @@ namespace nadena.dev.modular_avatar.core.editor Localization.ShowLanguageUI(); } } -} \ No newline at end of file +} diff --git a/Editor/Inspector/LogoDisplay.cs b/Editor/Inspector/LogoDisplay.cs index c583b297..43b55051 100644 --- a/Editor/Inspector/LogoDisplay.cs +++ b/Editor/Inspector/LogoDisplay.cs @@ -18,7 +18,7 @@ namespace nadena.dev.modular_avatar.core.editor { return (EditorStyles.label?.lineHeight ?? 0) * 3; } - catch (NullReferenceException e) + catch (NullReferenceException) { // This can happen in early initialization... return 0; diff --git a/Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs b/Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs index d13532c4..c7410eb8 100644 --- a/Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs +++ b/Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs @@ -19,7 +19,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger protected override void OnInnerInspectorGUI() { - throw new NotImplementedException(); + EditorGUILayout.HelpBox("Unable to show override changes", MessageType.Info); } protected override VisualElement CreateInnerInspectorGUI() diff --git a/Editor/Inspector/MaterialSetter/MaterialSetterStyles.uss b/Editor/Inspector/MaterialSetter/MaterialSetterStyles.uss index c0ef6413..84204231 100644 --- a/Editor/Inspector/MaterialSetter/MaterialSetterStyles.uss +++ b/Editor/Inspector/MaterialSetter/MaterialSetterStyles.uss @@ -1,7 +1,4 @@ -VisualElement { -} - -#group-box { +#group-box { margin-top: 4px; margin-bottom: 4px; padding: 4px; @@ -14,21 +11,36 @@ /* background-color: rgba(0, 0, 0, 0.1); */ } -#ListViewContainer { - margin-top: 4px; -} - #group-box > Label { -unity-font-style: bold; } -.horizontal { - flex-direction: row; +#ListViewContainer { + margin-top: 4px; } -.horizontal #f-object { +.horizontal { + flex-direction: row; + align-items: center; + justify-content: space-between; + margin: 1px 0; +} + +.horizontal > * { + height: 18px; + margin: 0 1px; +} + +.horizontal > Label { + height: auto; +} + +.horizontal > PropertyField > * { + margin: 0; +} + +#f-object { flex-grow: 1; - margin-bottom: 1px; } #f-material-index { @@ -43,14 +55,10 @@ flex-grow: 1; } +.horizontal > Label { + width: 100px; +} + #f-material { flex-grow: 1; } - -.horizontal > Label { - align-self: center; - width: 100px; - height: 19px; - margin: 1px -2px 1px 3px; - -unity-text-align: middle-left; -} \ No newline at end of file diff --git a/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.cs b/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.cs index f789cc29..95013d55 100644 --- a/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.cs +++ b/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.cs @@ -3,7 +3,6 @@ using UnityEditor; using UnityEditor.UIElements; using UnityEngine; -using UnityEngine.UI; using UnityEngine.UIElements; #endregion @@ -148,7 +147,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger { return targetObject?.GetComponent()?.sharedMaterials; } - catch (MissingComponentException e) + catch (MissingComponentException) { return null; } diff --git a/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.uxml b/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.uxml index 51af25c4..bc716db3 100644 --- a/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.uxml +++ b/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.uxml @@ -1,16 +1,16 @@  - - - - - - - - - - - - - + + - \ No newline at end of file + + + + + + + + + + + + diff --git a/Editor/Inspector/Menu/MenuItemGUI.cs b/Editor/Inspector/Menu/MenuItemGUI.cs index 04bf2eb7..c89c3817 100644 --- a/Editor/Inspector/Menu/MenuItemGUI.cs +++ b/Editor/Inspector/Menu/MenuItemGUI.cs @@ -2,10 +2,12 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Runtime.Serialization; using nadena.dev.modular_avatar.core.menu; using nadena.dev.ndmf; +using nadena.dev.ndmf.preview; using UnityEditor; using UnityEngine; using VRC.SDK3.Avatars.Components; @@ -21,6 +23,40 @@ namespace nadena.dev.modular_avatar.core.editor protected override string localizationPrefix => "submenu_source"; } + internal static class ParameterIntrospectionCache + { + internal static PropCache> ProvidedParameterCache = + new("GetParametersForObject", GetParametersForObject_miss); + + internal static PropCache> + ParameterRemappingCache = new("GetParameterRemappingsAt", GetParameterRemappingsAt_miss); + + private static ImmutableList GetParametersForObject_miss(ComputeContext ctx, GameObject obj) + { + if (obj == null) return ImmutableList.Empty; + + return ParameterInfo.ForPreview(ctx).GetParametersForObject(obj).ToImmutableList(); + } + + private static ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> + GetParameterRemappingsAt_miss(ComputeContext ctx, GameObject obj) + { + if (obj == null) return ImmutableDictionary<(ParameterNamespace, string), ParameterMapping>.Empty; + + return ParameterInfo.ForPreview(ctx).GetParameterRemappingsAt(obj); + } + + internal static ImmutableList GetParametersForObject(GameObject avatar) + { + return ProvidedParameterCache.Get(ComputeContext.NullContext, avatar); + } + + internal static ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> GetParameterRemappingsAt(GameObject avatar) + { + return ParameterRemappingCache.Get(ComputeContext.NullContext, avatar); + } + } + internal class MenuItemCoreGUI { private static readonly ObjectIDGenerator IdGenerator = new ObjectIDGenerator(); @@ -54,6 +90,7 @@ namespace nadena.dev.modular_avatar.core.editor private readonly SerializedProperty _prop_isSynced; private readonly SerializedProperty _prop_isSaved; private readonly SerializedProperty _prop_isDefault; + private readonly SerializedProperty _prop_automaticValue; public bool AlwaysExpandContents = false; public bool ExpandContents = false; @@ -105,6 +142,7 @@ namespace nadena.dev.modular_avatar.core.editor _prop_isSynced = obj.FindProperty(nameof(ModularAvatarMenuItem.isSynced)); _prop_isSaved = obj.FindProperty(nameof(ModularAvatarMenuItem.isSaved)); _prop_isDefault = obj.FindProperty(nameof(ModularAvatarMenuItem.isDefault)); + _prop_automaticValue = obj.FindProperty(nameof(ModularAvatarMenuItem.automaticValue)); _previewGUI = new MenuPreviewGUI(redraw); } @@ -132,12 +170,12 @@ namespace nadena.dev.modular_avatar.core.editor Dictionary rootParameters = new(); - foreach (var param in ParameterInfo.ForUI.GetParametersForObject(parentAvatar.gameObject) + foreach (var param in ParameterIntrospectionCache.GetParametersForObject(parentAvatar.gameObject) .Where(p => p.Namespace == ParameterNamespace.Animator) ) rootParameters[param.EffectiveName] = param; - var remaps = ParameterInfo.ForUI.GetParameterRemappingsAt(paramRef); + var remaps = ParameterIntrospectionCache.GetParameterRemappingsAt(paramRef); foreach (var remap in remaps) { if (remap.Key.Item1 != ParameterNamespace.Animator) continue; @@ -180,6 +218,7 @@ namespace nadena.dev.modular_avatar.core.editor _prop_isSynced = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isSynced)); _prop_isSaved = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isSaved)); _prop_isDefault = _control.FindPropertyRelative(nameof(ModularAvatarMenuItem.isDefault)); + _prop_automaticValue = null; _prop_submenuSource = null; _prop_otherObjSource = null; @@ -202,22 +241,17 @@ namespace nadena.dev.modular_avatar.core.editor if (forceMixedValues != null) EditorGUI.showMixedValue = forceMixedValues.Value; - if (forceValue != null) - { - EditorGUI.ToggleLeft(rect, label, forceValue.Value); - } - else - { - EditorGUI.BeginChangeCheck(); - var value = EditorGUI.ToggleLeft(rect, label, prop.boolValue); - if (EditorGUI.EndChangeCheck()) prop.boolValue = value; - } - + EditorGUI.BeginChangeCheck(); + var value = EditorGUI.ToggleLeft(rect, label, forceValue ?? prop.boolValue); + if (EditorGUI.EndChangeCheck()) prop.boolValue = value; + EditorGUI.EndProperty(); } public void DoGUI() { + if (_obj != null) _obj.UpdateIfRequiredOrScript(); + EditorGUILayout.BeginHorizontal(); EditorGUILayout.BeginVertical(); @@ -230,7 +264,7 @@ namespace nadena.dev.modular_avatar.core.editor EditorGUILayout.PropertyField(_texture, G("menuitem.prop.icon")); EditorGUILayout.PropertyField(_type, G("menuitem.prop.type")); - EditorGUILayout.PropertyField(_value, G("menuitem.prop.value")); + DoValueField(); _parameterGUI.DoGUI(true); @@ -436,22 +470,44 @@ namespace nadena.dev.modular_avatar.core.editor // For now, don't show the UI in this case. return; + var multipleSelections = _obj.targetObjects.Length > 1; + var paramName = _parameterName.stringValue; + var siblings = FindSiblingMenuItems(_obj); EditorGUILayout.BeginHorizontal(); - bool? forceMixedValues = _parameterName.hasMultipleDifferentValues ? true : null; + var forceMixedValues = _parameterName.hasMultipleDifferentValues; + + var syncedIsMixed = forceMixedValues || _prop_isSynced.hasMultipleDifferentValues || + siblings != null && siblings.Any(s => s.isSynced != _prop_isSynced.boolValue); + var savedIsMixed = forceMixedValues || _prop_isSaved.hasMultipleDifferentValues || + siblings != null && siblings.Any(s => s.isSaved != _prop_isSaved.boolValue); + var knownParameter = _parameterName.hasMultipleDifferentValues ? null : _knownParameters.GetValueOrDefault(paramName); + var knownSource = knownParameter?.Source; + var externalSource = knownSource != null && knownSource is not ModularAvatarMenuItem; + + if (externalSource) savedIsMixed = true; // NDMF doesn't yet support querying for the saved state + var forceSyncedValue = externalSource ? knownParameter?.WantSynced : null; + var knownParamDefault = knownParameter?.DefaultValue; var isDefaultByKnownParam = knownParamDefault != null ? _value.floatValue == knownParamDefault : (bool?)null; + + if (knownParameter != null && knownParameter.Source is ModularAvatarMenuItem) + isDefaultByKnownParam = null; + + if (_prop_automaticValue?.boolValue == true) isDefaultByKnownParam = null; + Object controller = knownParameter?.Source; - var controllerIsElsewhere = controller != null && !(controller is ModularAvatarMenuItem); - // If we can't figure out what to reference the parameter names to, disable the UI - controllerIsElsewhere = controllerIsElsewhere || _parameterSourceNotDetermined; + + // If we can't figure out what to reference the parameter names to, or if they're controlled by something + // other than the Menu Item component itself, disable the UI + var controllerIsElsewhere = externalSource || _parameterSourceNotDetermined; using (new EditorGUI.DisabledScope( _parameterName.hasMultipleDifferentValues || controllerIsElsewhere) @@ -460,22 +516,42 @@ namespace nadena.dev.modular_avatar.core.editor // If we have multiple menu items selected, it probably doesn't make sense to make them all default. // But, we do want to see if _any_ are default. var anyIsDefault = _prop_isDefault.hasMultipleDifferentValues || _prop_isDefault.boolValue; - var multipleSelections = _obj.targetObjects.Length > 1; var mixedIsDefault = multipleSelections && anyIsDefault; - using (new EditorGUI.DisabledScope(multipleSelections)) + using (new EditorGUI.DisabledScope(multipleSelections || isDefaultByKnownParam != null)) { + EditorGUI.BeginChangeCheck(); DrawHorizontalToggleProp(_prop_isDefault, G("menuitem.prop.is_default"), mixedIsDefault, - multipleSelections ? false : isDefaultByKnownParam); + isDefaultByKnownParam); + if (EditorGUI.EndChangeCheck()) + { + _obj.ApplyModifiedProperties(); + ClearConflictingDefaults(siblings); + } } GUILayout.FlexibleSpace(); - var isSavedMixed = forceMixedValues ?? - (_parameterName.hasMultipleDifferentValues || controllerIsElsewhere ? true : null); - DrawHorizontalToggleProp(_prop_isSaved, G("menuitem.prop.is_saved"), isSavedMixed); + EditorGUI.BeginChangeCheck(); + DrawHorizontalToggleProp(_prop_isSaved, G("menuitem.prop.is_saved"), savedIsMixed); + if (EditorGUI.EndChangeCheck() && siblings != null) + foreach (var sibling in siblings) + { + sibling.isSaved = _prop_isSaved.boolValue; + EditorUtility.SetDirty(sibling); + PrefabUtility.RecordPrefabInstancePropertyModifications(sibling); + } + GUILayout.FlexibleSpace(); - DrawHorizontalToggleProp(_prop_isSynced, G("menuitem.prop.is_synced"), forceMixedValues, - knownParameter?.WantSynced); + EditorGUI.BeginChangeCheck(); + DrawHorizontalToggleProp(_prop_isSynced, G("menuitem.prop.is_synced"), syncedIsMixed, + forceSyncedValue); + if (EditorGUI.EndChangeCheck() && siblings != null) + foreach (var sibling in siblings) + { + sibling.isSynced = _prop_isSynced.boolValue; + EditorUtility.SetDirty(sibling); + PrefabUtility.RecordPrefabInstancePropertyModifications(sibling); + } } if (controllerIsElsewhere) @@ -514,6 +590,109 @@ namespace nadena.dev.modular_avatar.core.editor EditorGUILayout.EndHorizontal(); } + private void DoValueField() + { + var value_label = G("menuitem.prop.value"); + var auto_label = G("menuitem.prop.automatic_value"); + + if (_prop_automaticValue == null) + { + EditorGUILayout.PropertyField(_value, value_label); + return; + } + + var toggleSize = EditorStyles.toggle.CalcSize(new GUIContent()); + var autoLabelSize = EditorStyles.label.CalcSize(auto_label); + + var style = EditorStyles.numberField; + var rect = EditorGUILayout.GetControlRect(true, EditorGUIUtility.singleLineHeight, style); + + var valueRect = rect; + valueRect.xMax -= toggleSize.x + autoLabelSize.x + 4; + + var autoRect = rect; + autoRect.xMin = valueRect.xMax + 4; + + var suppressValue = _prop_automaticValue.boolValue || _prop_automaticValue.hasMultipleDifferentValues; + + using (new EditorGUI.DisabledScope(suppressValue)) + { + if (suppressValue) + { + EditorGUI.TextField(valueRect, value_label, "", style); + } + else + { + EditorGUI.BeginChangeCheck(); + EditorGUI.PropertyField(valueRect, _value, value_label); + if (EditorGUI.EndChangeCheck()) _prop_automaticValue.boolValue = false; + } + } + + EditorGUI.BeginProperty(autoRect, auto_label, _prop_automaticValue); + EditorGUI.BeginChangeCheck(); + + EditorGUI.showMixedValue = _prop_automaticValue.hasMultipleDifferentValues; + var autoValue = EditorGUI.ToggleLeft(autoRect, auto_label, _prop_automaticValue.boolValue); + + if (EditorGUI.EndChangeCheck()) _prop_automaticValue.boolValue = autoValue; + EditorGUI.EndProperty(); + } + + private List FindSiblingMenuItems(SerializedObject serializedObject) + { + if (serializedObject == null || serializedObject.isEditingMultipleObjects) return null; + + var myMenuItem = serializedObject.targetObject as ModularAvatarMenuItem; + if (myMenuItem == null) return null; + + var myParameterName = myMenuItem.Control.parameter.name; + if (string.IsNullOrEmpty(myParameterName)) return new List(); + + var myMappings = ParameterIntrospectionCache.GetParameterRemappingsAt(myMenuItem.gameObject); + if (myMappings.TryGetValue((ParameterNamespace.Animator, myParameterName), out var myReplacement)) + myParameterName = myReplacement.ParameterName; + + var avatarRoot = RuntimeUtil.FindAvatarInParents(myMenuItem.gameObject.transform); + var siblings = new List(); + + foreach (var otherMenuItem in avatarRoot.GetComponentsInChildren(true)) + { + if (otherMenuItem == myMenuItem) continue; + + var otherParameterName = otherMenuItem.Control.parameter.name; + if (string.IsNullOrEmpty(otherParameterName)) continue; + + var otherMappings = ParameterIntrospectionCache.GetParameterRemappingsAt(otherMenuItem.gameObject); + if (otherMappings.TryGetValue((ParameterNamespace.Animator, otherParameterName), + out var otherReplacement)) + otherParameterName = otherReplacement.ParameterName; + + if (otherParameterName != myParameterName) continue; + + siblings.Add(otherMenuItem); + } + + return siblings; + } + + private void ClearConflictingDefaults(List siblingItems) + { + var siblings = siblingItems; + if (siblings == null) return; + + foreach (var otherMenuItem in siblings) + { + if (otherMenuItem.isDefault) + { + Undo.RecordObject(otherMenuItem, ""); + otherMenuItem.isDefault = false; + EditorUtility.SetDirty(otherMenuItem); + PrefabUtility.RecordPrefabInstancePropertyModifications(otherMenuItem); + } + } + } + private void EnsureLabelCount(int i) { if (_labels == null || _labelsRoot.arraySize < i || _labels.Length < i) diff --git a/Editor/Inspector/Menu/MenuPreviewGUI.cs b/Editor/Inspector/Menu/MenuPreviewGUI.cs index 9a2fe0ab..bd3e4e25 100644 --- a/Editor/Inspector/Menu/MenuPreviewGUI.cs +++ b/Editor/Inspector/Menu/MenuPreviewGUI.cs @@ -212,7 +212,10 @@ namespace nadena.dev.modular_avatar.core.editor var newChild = new GameObject(); newChild.name = "New item"; newChild.transform.SetParent(nodesUnder.root.transform, false); - newChild.AddComponent(); + + var mami = newChild.AddComponent(); + mami.InitSettings(); + Undo.RegisterCreatedObjectUndo(newChild, "Added menu item"); } @@ -223,13 +226,12 @@ namespace nadena.dev.modular_avatar.core.editor newChild.transform.SetParent(nodesUnder.root.transform, false); var mami = newChild.AddComponent(); + mami.InitSettings(); mami.Control = new VRCExpressionsMenu.Control() { type = VRCExpressionsMenu.Control.ControlType.Toggle, value = 1, }; - mami.isSaved = true; - mami.isSynced = true; newChild.AddComponent(); diff --git a/Editor/Inspector/Menu/ToggleCreatorShortcut.cs b/Editor/Inspector/Menu/ToggleCreatorShortcut.cs index 56105421..e549d319 100644 --- a/Editor/Inspector/Menu/ToggleCreatorShortcut.cs +++ b/Editor/Inspector/Menu/ToggleCreatorShortcut.cs @@ -30,7 +30,7 @@ namespace nadena.dev.modular_avatar.core.editor createInstaller = false; } } - catch (MissingComponentException e) + catch (MissingComponentException) { // ignore } @@ -40,7 +40,10 @@ namespace nadena.dev.modular_avatar.core.editor var objToggle = toggle.AddComponent(); toggle.transform.SetParent(parent, false); - toggle.AddComponent().Control = new VRCExpressionsMenu.Control + + var mami = toggle.AddComponent(); + mami.InitSettings(); + mami.Control = new VRCExpressionsMenu.Control { type = VRCExpressionsMenu.Control.ControlType.Toggle, name = "New Toggle", diff --git a/Editor/Inspector/MoveIndependently/MoveIndependentlyEditor.cs b/Editor/Inspector/MoveIndependently/MoveIndependentlyEditor.cs index 8c6da1a7..4b741d90 100644 --- a/Editor/Inspector/MoveIndependently/MoveIndependentlyEditor.cs +++ b/Editor/Inspector/MoveIndependently/MoveIndependentlyEditor.cs @@ -23,7 +23,7 @@ namespace nadena.dev.modular_avatar.core.editor protected override void OnInnerInspectorGUI() { - throw new NotImplementedException(); + EditorGUILayout.HelpBox("Unable to show override changes", MessageType.Info); } protected override VisualElement CreateInnerInspectorGUI() @@ -61,7 +61,7 @@ namespace nadena.dev.modular_avatar.core.editor var grouped = ctx.Observe(target, t => (t.GroupedBones ?? Array.Empty()) .Select(obj => obj.transform) - .ToHashSet(new ObjectIdentityComparer()), + .ToHashSet(), (x, y) => x.SetEquals(y) ); diff --git a/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs b/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs index e954dda7..627121b7 100644 --- a/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs +++ b/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs @@ -1,6 +1,7 @@ #region using System; +using System.Linq; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; @@ -16,10 +17,11 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger [SerializeField] private StyleSheet uss; [SerializeField] private VisualTreeAsset uxml; + private DragAndDropManipulator _dragAndDropManipulator; protected override void OnInnerInspectorGUI() { - throw new NotImplementedException(); + EditorGUILayout.HelpBox("Unable to show override changes", MessageType.Info); } protected override VisualElement CreateInnerInspectorGUI() @@ -33,11 +35,109 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger ROSimulatorButton.BindRefObject(root, target); var listView = root.Q("Shapes"); + _dragAndDropManipulator = new DragAndDropManipulator(listView) + { + TargetComponent = target as ModularAvatarObjectToggle + }; listView.showBoundCollectionSize = false; listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight; return root; } + + private void OnEnable() + { + if (_dragAndDropManipulator != null) + _dragAndDropManipulator.TargetComponent = target as ModularAvatarObjectToggle; + } + + private class DragAndDropManipulator : PointerManipulator + { + public ModularAvatarObjectToggle TargetComponent; + private GameObject[] _nowDragging = Array.Empty(); + private Transform _avatarRoot; + + private readonly VisualElement _parentElem; + + public DragAndDropManipulator(VisualElement target) + { + this.target = target; + _parentElem = target.parent; + } + + protected override void RegisterCallbacksOnTarget() + { + target.RegisterCallback(OnDragEnter); + target.RegisterCallback(OnDragLeave); + target.RegisterCallback(OnDragPerform); + target.RegisterCallback(OnDragUpdate); + } + + protected override void UnregisterCallbacksFromTarget() + { + target.UnregisterCallback(OnDragEnter); + target.UnregisterCallback(OnDragLeave); + target.UnregisterCallback(OnDragPerform); + target.RegisterCallback(OnDragUpdate); + } + + + private void OnDragEnter(DragEnterEvent evt) + { + if (TargetComponent == null) return; + + _avatarRoot = RuntimeUtil.FindAvatarTransformInParents(TargetComponent.transform); + if (_avatarRoot == null) return; + + _nowDragging = DragAndDrop.objectReferences.OfType() + .Where(o => RuntimeUtil.FindAvatarTransformInParents(o.transform) == _avatarRoot) + .ToArray(); + + if (_nowDragging.Length > 0) + { + DragAndDrop.visualMode = DragAndDropVisualMode.Link; + + _parentElem.AddToClassList("drop-area--drag-active"); + } + } + + private void OnDragUpdate(DragUpdatedEvent _) + { + if (_nowDragging.Length > 0) DragAndDrop.visualMode = DragAndDropVisualMode.Link; + } + + private void OnDragLeave(DragLeaveEvent evt) + { + _nowDragging = Array.Empty(); + _parentElem.RemoveFromClassList("drop-area--drag-active"); + } + + private void OnDragPerform(DragPerformEvent evt) + { + if (_nowDragging.Length > 0 && TargetComponent != null && _avatarRoot != null) + { + var knownObjs = TargetComponent.Objects.Select(o => o.Object.Get(TargetComponent)).ToHashSet(); + + Undo.RecordObject(TargetComponent, "Add Toggled Objects"); + foreach (var obj in _nowDragging) + { + if (knownObjs.Contains(obj)) continue; + + var aor = new AvatarObjectReference(); + aor.Set(obj); + + var toggledObject = new ToggledObject { Object = aor, Active = !obj.activeSelf }; + TargetComponent.Objects.Add(toggledObject); + } + + EditorUtility.SetDirty(TargetComponent); + PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent); + } + + _nowDragging = Array.Empty(); + _parentElem.RemoveFromClassList("drop-area--drag-active"); + } + } } } \ No newline at end of file diff --git a/Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss b/Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss index db197753..e304365a 100644 --- a/Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss +++ b/Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss @@ -1,7 +1,4 @@ -VisualElement { -} - -#group-box { +#group-box { margin-top: 4px; margin-bottom: 4px; padding: 4px; @@ -14,101 +11,42 @@ /* background-color: rgba(0, 0, 0, 0.1); */ } -#ListViewContainer { - margin-top: 4px; -} - #group-box > Label { -unity-font-style: bold; } -.group-root { +#ListViewContainer { margin-top: 4px; } -.group-root Toggle { - margin-left: 0; -} - -.group-children { - padding-left: 10px; -} - -.left-toggle { - display: flex; +.horizontal { flex-direction: row; -} - -.toggled-object-editor { - flex-direction: row; - justify-content: center; align-items: center; + justify-content: space-between; + margin: 1px 0; } -.toggled-object-editor #f-object { +.horizontal > * { + height: 18px; + margin: 0 1px; +} + +.horizontal > Label { + height: auto; +} + +.horizontal > PropertyField > * { + margin: 0; +} + +#f-active { + justify-content: center; +} + +#f-object { flex-grow: 1; - height: 100%; } -#f-active > Toggle { - margin-top: 0; - margin-bottom: 0; - margin-left: -12px; - margin-right: 3px; -} - -.toggled-object-editor PropertyField Label { - display: none; -} - -#f-change-type { - width: 75px; -} - -.f-value { - width: 40px; -} - -#f-value-delete { - display: none; -} - -.change-type-delete #f-value { - display: none; -} - -.change-type-delete #f-value-delete { - display: flex; -} - -/* Add shape window */ - -.add-shape-popup { - margin: 2px; -} - -.vline { - width: 100%; - height: 4px; - border-top-width: 4px; - margin-top: 2px; - margin-bottom: 2px; - border-top-color: rgba(0, 0, 0, 0.2); -} - -.add-shape-row { - flex-direction: row; -} - -.add-shape-row Button { - flex-grow: 0; -} - -.add-shape-popup ScrollView Label.placeholder { - -unity-text-align: middle-center; -} - -.add-shape-row Label { - flex-grow: 1; - -unity-text-align: middle-left; +.drop-area--drag-active > ListView ScrollView { + background-color: rgba(0, 255, 255, 0.1); } diff --git a/Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml b/Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml index b7f790ba..0705c9b5 100644 --- a/Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml +++ b/Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml @@ -1,6 +1,6 @@  - - - + + + - \ No newline at end of file + diff --git a/Editor/Inspector/Parameters/DefaultValueField.cs b/Editor/Inspector/Parameters/DefaultValueField.cs index 6cae5283..6ba6cd22 100644 --- a/Editor/Inspector/Parameters/DefaultValueField.cs +++ b/Editor/Inspector/Parameters/DefaultValueField.cs @@ -1,6 +1,4 @@ using System.Globalization; -using UnityEditor; -using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; @@ -12,97 +10,107 @@ namespace nadena.dev.modular_avatar.core.editor { } + private const string V_None = " "; private const string V_True = "ON"; private const string V_False = "OFF"; - - private readonly TextField _visibleField; + private readonly FloatField _defaultValueField; + private readonly Toggle _hasExplicitDefaultValueField; + private readonly TextField _numberField; private readonly DropdownField _boolField; - private readonly Toggle _hasExplicitDefaultSetField; + + private ParameterSyncType _syncType; public DefaultValueField() { // Hidden binding elements _defaultValueField = new FloatField(); - _hasExplicitDefaultSetField = new Toggle(); - _boolField = new DropdownField(); + _defaultValueField.style.display = DisplayStyle.None; + _defaultValueField.bindingPath = nameof(ParameterConfig.defaultValue); + _defaultValueField.RegisterValueChangedCallback(evt => UpdateVisibleField(evt.newValue, _hasExplicitDefaultValueField.value)); + _hasExplicitDefaultValueField = new Toggle(); + _hasExplicitDefaultValueField.style.display = DisplayStyle.None; + _hasExplicitDefaultValueField.bindingPath = nameof(ParameterConfig.hasExplicitDefaultValue); + _hasExplicitDefaultValueField.RegisterValueChangedCallback(evt => UpdateVisibleField(_defaultValueField.value, evt.newValue)); - _boolField.choices.Add(""); + // Visible elements for input + _numberField = new TextField(); + _numberField.isDelayed = true; + _numberField.RegisterValueChangedCallback(evt => OnUpdateNumberValue(evt.newValue)); + _boolField = new DropdownField(); + _boolField.choices.Add(V_None); _boolField.choices.Add(V_True); _boolField.choices.Add(V_False); + _boolField.RegisterValueChangedCallback(evt => OnUpdateBoolValue(evt.newValue)); - _defaultValueField.RegisterValueChangedCallback( - evt => UpdateVisibleField(evt.newValue, _hasExplicitDefaultSetField.value)); - _defaultValueField.bindingPath = nameof(ParameterConfig.defaultValue); - - _hasExplicitDefaultSetField.RegisterValueChangedCallback( - evt => UpdateVisibleField(_defaultValueField.value, evt.newValue)); - _hasExplicitDefaultSetField.bindingPath = nameof(ParameterConfig.hasExplicitDefaultValue); - - _visibleField = new TextField(); - _visibleField.RegisterValueChangedCallback(evt => - { - if (string.IsNullOrWhiteSpace(evt.newValue)) - { - _hasExplicitDefaultSetField.value = false; - _defaultValueField.value = 0; - } - else - { - _hasExplicitDefaultSetField.value = true; - _defaultValueField.value = float.Parse(evt.newValue, CultureInfo.InvariantCulture); - } - }); - - _defaultValueField.style.width = 0; - _defaultValueField.SetEnabled(false); - _hasExplicitDefaultSetField.style.width = 0; - _hasExplicitDefaultSetField.SetEnabled(false); - - _boolField.RegisterValueChangedCallback(evt => - { - if (evt.newValue == V_True) - _defaultValueField.value = 1; - else - _defaultValueField.value = 0; - - _hasExplicitDefaultSetField.value = evt.newValue != ""; - }); - - - style.flexDirection = FlexDirection.Row; - - Add(_visibleField); - Add(_boolField); Add(_defaultValueField); - Add(_hasExplicitDefaultSetField); + Add(_hasExplicitDefaultValueField); + Add(_numberField); + Add(_boolField); } - public void ManualBindProperty(SerializedProperty property) + public void OnUpdateSyncType(ParameterSyncType syncType) { - _defaultValueField.BindProperty(property); - _hasExplicitDefaultSetField.BindProperty(property); + _syncType = syncType; + + if (syncType != ParameterSyncType.Bool) + { + _numberField.style.display = DisplayStyle.Flex; + _boolField.style.display = DisplayStyle.None; + OnUpdateNumberValue(_numberField.value); + } + else + { + _numberField.style.display = DisplayStyle.None; + _boolField.style.display = DisplayStyle.Flex; + OnUpdateBoolValue(_boolField.value); + } } - + + private void OnUpdateNumberValue(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + _defaultValueField.value = 0; + _hasExplicitDefaultValueField.value = false; + } + else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) + && !float.IsNaN(parsed) + && !float.IsInfinity(parsed)) + { + _defaultValueField.value = _syncType switch + { + ParameterSyncType.Int => Mathf.FloorToInt(Mathf.Clamp(parsed, 0, 255)), + ParameterSyncType.Float => Mathf.Clamp(parsed, -1, 1), + ParameterSyncType.Bool => parsed != 0 ? 1 : 0, + _ => parsed, + }; + _hasExplicitDefaultValueField.value = true; + } + + UpdateVisibleField(_defaultValueField.value, _hasExplicitDefaultValueField.value); + } + + private void OnUpdateBoolValue(string value) + { + _defaultValueField.value = value == V_True ? 1 : 0; + _hasExplicitDefaultValueField.value = value != V_None; + + UpdateVisibleField(_defaultValueField.value, _hasExplicitDefaultValueField.value); + } + private void UpdateVisibleField(float value, bool hasExplicitValue) { - if (Mathf.Abs(value) > 0.0000001) + if (hasExplicitValue || Mathf.Abs(value) > 0.0000001) { - hasExplicitValue = true; + _numberField.SetValueWithoutNotify(value.ToString(CultureInfo.InvariantCulture)); + _boolField.SetValueWithoutNotify(value != 0 ? V_True : V_False); } - - var str = hasExplicitValue ? value.ToString(CultureInfo.InvariantCulture) : ""; - _visibleField.SetValueWithoutNotify(str); - - string boolStr; - if (!hasExplicitValue) - boolStr = ""; - else if (value > 0.5) - boolStr = V_True; else - boolStr = V_False; - - _boolField.SetValueWithoutNotify(boolStr); + { + _numberField.SetValueWithoutNotify(string.Empty); + _boolField.SetValueWithoutNotify(V_None); + } } } -} \ No newline at end of file +} diff --git a/Editor/Inspector/Parameters/ParameterConfigDrawer.cs b/Editor/Inspector/Parameters/ParameterConfigDrawer.cs index 101716e2..09bff39d 100644 --- a/Editor/Inspector/Parameters/ParameterConfigDrawer.cs +++ b/Editor/Inspector/Parameters/ParameterConfigDrawer.cs @@ -20,15 +20,14 @@ namespace nadena.dev.modular_avatar.core.editor.Parameters Localization.UI.Localize(root); root.styleSheets.Add(uss); - var proot = root.Q("Root"); - var type_field = proot.Q("f-type"); - - var f_sync_type = proot.Q("f-sync-type"); + var f_type = root.Q("f-type"); + var f_sync_type = root.Q("f-sync-type"); + var f_is_prefix = root.Q("f-is-prefix"); SetupPairedDropdownField( - proot, - type_field, + root, + f_type, f_sync_type, - proot.Q("f-is-prefix"), + f_is_prefix, ("Bool", "False", "params.syncmode.Bool"), ("Float", "False", "params.syncmode.Float"), ("Int", "False", "params.syncmode.Int"), @@ -36,54 +35,51 @@ namespace nadena.dev.modular_avatar.core.editor.Parameters (null, "True", "params.syncmode.PhysBonesPrefix") ); - f_sync_type.Q().RegisterValueChangedCallback(evt => - { - var is_anim_only = evt.newValue == "Not Synced"; + var f_default = root.Q(); + f_default.OnUpdateSyncType((ParameterSyncType)f_sync_type.index); + f_sync_type.RegisterValueChangedCallback(evt => f_default.OnUpdateSyncType((ParameterSyncType)f_sync_type.index)); - if (is_anim_only) - proot.AddToClassList("st-anim-only"); - else - proot.RemoveFromClassList("st-anim-only"); - }); - - var f_synced = proot.Q("f-synced"); - var f_local_only = proot.Q("f-local-only"); + var f_synced = root.Q("f-synced"); + var f_local_only = root.Q("f-local-only"); // Invert f_local_only and f_synced f_local_only.RegisterValueChangedCallback(evt => { f_synced.SetValueWithoutNotify(!evt.newValue); }); f_synced.RegisterValueChangedCallback(evt => { f_local_only.value = !evt.newValue; }); - var internalParamAccessor = proot.Q("f-internal-parameter"); + var internalParamAccessor = root.Q("f-internal-parameter"); internalParamAccessor.RegisterValueChangedCallback(evt => { if (evt.newValue) - proot.AddToClassList("st-internal-parameter"); + root.AddToClassList("st-internal-parameter"); else - proot.RemoveFromClassList("st-internal-parameter"); + root.RemoveFromClassList("st-internal-parameter"); }); - var remapTo = proot.Q("f-remap-to"); - var defaultParam = proot.Q