using System; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using UnityEditor; using UnityEditor.Animations; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using VF.Builder; using VF.Feature.Base; using VF.Inspector; using VF.Model.Feature; namespace VF.Feature { public class GestureDriverBuilder : FeatureBuilder { private int i = 0; private readonly Dictionary lockMenuItems = new Dictionary(); private readonly Dictionary excludeConditions = new Dictionary(); [FeatureBuilderAction] public void Apply() { foreach (var gesture in model.gestures) { MakeGesture(gesture); } } private void MakeGesture(GestureDriver.Gesture gesture, GestureDriver.Hand handOverride = GestureDriver.Hand.EITHER) { var hand = handOverride == GestureDriver.Hand.EITHER ? gesture.hand : handOverride; if (gesture.enableWeight && hand == GestureDriver.Hand.EITHER && gesture.sign == GestureDriver.HandSign.FIST) { MakeGesture(gesture, GestureDriver.Hand.LEFT); MakeGesture(gesture, GestureDriver.Hand.RIGHT); return; } var fx = GetFx(); var uniqueNum = i++; var name = "Gesture " + uniqueNum + " - " + hand + " " + gesture.sign; if (hand == GestureDriver.Hand.COMBO) { name += " " + gesture.comboSign; } var uid = "gesture_" + uniqueNum; var layer = fx.NewLayer(name); var off = layer.NewState("Off"); var on = layer.NewState("On"); VFABool lockMenuParam = null; if (gesture.enableLockMenuItem && !string.IsNullOrWhiteSpace(gesture.lockMenuItem)) { if (!lockMenuItems.TryGetValue(gesture.lockMenuItem, out lockMenuParam)) { // This doesn't actually need synced, but vrc gets annoyed if the menu is using an unsynced param lockMenuParam = fx.NewBool(uid + "_lock", synced: true); manager.GetMenu().NewMenuToggle(gesture.lockMenuItem, lockMenuParam); lockMenuItems[gesture.lockMenuItem] = lockMenuParam; } } var GestureLeft = fx.GestureLeft(); var GestureRight = fx.GestureRight(); VFACondition onCondition; int weightHand = 0; if (hand == GestureDriver.Hand.LEFT) { onCondition = GestureLeft.IsEqualTo((int)gesture.sign); if (gesture.sign == GestureDriver.HandSign.FIST) weightHand = 1; } else if (hand == GestureDriver.Hand.RIGHT) { onCondition = GestureRight.IsEqualTo((int)gesture.sign); if (gesture.sign == GestureDriver.HandSign.FIST) weightHand = 2; } else if (hand == GestureDriver.Hand.EITHER) { onCondition = GestureLeft.IsEqualTo((int)gesture.sign).Or(GestureRight.IsEqualTo((int)gesture.sign)); } else if (hand == GestureDriver.Hand.COMBO) { onCondition = GestureLeft.IsEqualTo((int)gesture.sign).And(GestureRight.IsEqualTo((int)gesture.comboSign)); if (gesture.comboSign == GestureDriver.HandSign.FIST) weightHand = 2; else if(gesture.sign == GestureDriver.HandSign.FIST) weightHand = 1; } else { throw new Exception("Unknown hand type"); } if (lockMenuParam != null) { onCondition = onCondition.Or(lockMenuParam.IsTrue()); } if (gesture.enableExclusiveTag) { foreach (var tag in gesture.exclusiveTag.Split(',')) { var trimmedTag = tag.Trim(); if (!string.IsNullOrWhiteSpace(trimmedTag)) { if (excludeConditions.TryGetValue(trimmedTag, out var excludeCondition)) { excludeConditions[trimmedTag] = excludeCondition.Or(onCondition); onCondition = onCondition.And(excludeCondition.Not()); } else { excludeConditions[trimmedTag] = onCondition; } } } } if (gesture.disableBlinking) { var disableBlinkParam = fx.NewBool(uid + "_disableBlink"); off.Drives(disableBlinkParam, false); on.Drives(disableBlinkParam, true); addOtherFeature(new BlinkingBuilder.BlinkingPrevention { param = disableBlinkParam }); } var clip = LoadState(uid, gesture.state); if (gesture.enableWeight && weightHand > 0) { MakeWeightParams(); var weightParam = weightHand == 1 ? leftWeightParam : rightWeightParam; var tree = manager.GetClipStorage().NewBlendTree(uid + "_blend"); tree.blendType = BlendTreeType.Simple1D; tree.useAutomaticThresholds = false; tree.blendParameter = weightParam.Name(); tree.AddChild(manager.GetClipStorage().GetNoopClip(), 0); tree.AddChild(clip, 1); on.WithAnimation(tree); } else { on.WithAnimation(clip); } var transitionTime = gesture.customTransitionTime && gesture.transitionTime >= 0 ? gesture.transitionTime : 0.1f; off.TransitionsTo(on).WithTransitionDurationSeconds(transitionTime).When(onCondition); on.TransitionsTo(off).WithTransitionDurationSeconds(transitionTime).When(onCondition.Not()); } private VFANumber leftWeightParam; private VFANumber rightWeightParam; private void MakeWeightParams() { if (leftWeightParam != null) return; var fx = GetFx(); var GestureLeftWeight = fx.GestureLeftWeight(); var GestureRightWeight = fx.GestureRightWeight(); var GestureLeftCondition = fx.GestureLeft().IsEqualTo(1); var GestureRightCondition = fx.GestureRight().IsEqualTo(1); leftWeightParam = MakeWeightLayer("left", GestureLeftWeight, GestureLeftCondition); rightWeightParam = MakeWeightLayer("right", GestureRightWeight, GestureRightCondition); } private VFANumber MakeWeightLayer(string name, VFANumber input, VFACondition whenEnabled) { var fx = GetFx(); var layer = fx.NewLayer("GestureWeight_" + name); var output = fx.NewFloat(input.Name() + "_cached"); // == BEGIN Smoothing logic // == Inspired by https://github.com/regzo2/OSCmooth //Values: 0 => no smoothing, 1 => no change in value, 0.999 => very smooth //TODO: maybe make this configurable and split between local/remote var localSmoothParam = fx.NewFloat(input.Name() + "_smooth_local", def: 0.65f); var remoteSmoothParam = fx.NewFloat(input.Name() + "_smooth_remote", def: 0.85f); //FeedbackClips - they drive the feedback values back to the output param var minClip = manager.GetClipStorage().NewClip(input.Name() + "-1"); minClip.SetCurve("", typeof(Animator), output.Name(), AnimationCurve.Constant(0, 0, -1f)); var maxClip = manager.GetClipStorage().NewClip(input.Name() + "1"); maxClip.SetCurve("", typeof(Animator), output.Name(), AnimationCurve.Constant(0, 0, 1f)); //Update tree - moves toward the target value var updateTree = manager.GetClipStorage().NewBlendTree("GestureWeight_" + name + "_input"); updateTree.blendType = BlendTreeType.Simple1D; updateTree.useAutomaticThresholds = false; updateTree.blendParameter = input.Name(); updateTree.AddChild(minClip, -1); updateTree.AddChild(maxClip, 1); //Maintain tree - maintains the current value var maintainTree = manager.GetClipStorage().NewBlendTree("GestureWeight_" + name + "_driver"); maintainTree.blendType = BlendTreeType.Simple1D; maintainTree.useAutomaticThresholds = false; maintainTree.blendParameter = output.Name(); maintainTree.AddChild(minClip, -1); maintainTree.AddChild(maxClip, 1); //The following two trees merge the update and the maintain tree together. The smoothParam controls //how much from either tree should be applied during each tick var localTree = manager.GetClipStorage().NewBlendTree("GestureWeight_" + name + "_root_local"); localTree.blendType = BlendTreeType.Simple1D; localTree.useAutomaticThresholds = false; localTree.blendParameter = localSmoothParam.Name(); localTree.AddChild(updateTree, 0); localTree.AddChild(maintainTree, 1); var remoteTree = manager.GetClipStorage().NewBlendTree("GestureWeight_" + name + "_root_remote"); remoteTree.blendType = BlendTreeType.Simple1D; remoteTree.useAutomaticThresholds = false; remoteTree.blendParameter = remoteSmoothParam.Name(); remoteTree.AddChild(updateTree, 0); remoteTree.AddChild(maintainTree, 1); var off = layer.NewState("Off"); var onLocal = layer.NewState("On Local").Move(off, -0.5f, 2f); var onRemote = layer.NewState("On Remote").Move(onLocal, 1f, 0f); var whenLocal = whenEnabled.And(fx.IsLocal().IsTrue()); var whenRemote = whenEnabled.And(fx.IsLocal().IsFalse()); var whenOff = whenLocal.Not().And(whenRemote.Not()); off.TransitionsTo(onLocal).When(whenLocal); off.TransitionsTo(onRemote).When(whenRemote); off.WithAnimation(maintainTree); onLocal.TransitionsTo(off).When(whenOff); onLocal.TransitionsTo(onRemote).When(whenRemote); onLocal.WithAnimation(localTree); onRemote.TransitionsTo(off).When(whenOff); onRemote.TransitionsTo(onLocal).When(whenLocal); onRemote.WithAnimation(remoteTree); return output; } public override string GetEditorTitle() { return "Gestures"; } public override VisualElement CreateEditor(SerializedProperty prop) { return VRCFuryEditorUtils.List(prop.FindPropertyRelative("gestures"), (i,el) => RenderGestureEditor(el)); } private VisualElement RenderGestureEditor(SerializedProperty gesture) { var wrapper = new VisualElement(); var handProp = gesture.FindPropertyRelative("hand"); var signProp = gesture.FindPropertyRelative("sign"); var comboSignProp = gesture.FindPropertyRelative("comboSign"); var row = new VisualElement(); row.style.flexDirection = FlexDirection.Row; var hand = VRCFuryEditorUtils.Prop(handProp); hand.style.flexBasis = 70; row.Add(hand); var handSigns = VRCFuryEditorUtils.RefreshOnChange(() => { var w = new VisualElement(); w.style.flexDirection = FlexDirection.Row; w.style.alignItems = Align.Center; var leftBox = VRCFuryEditorUtils.Prop(signProp); var rightBox = VRCFuryEditorUtils.Prop(comboSignProp); if ((GestureDriver.Hand)handProp.enumValueIndex == GestureDriver.Hand.COMBO) { w.Add(new Label("L") { style = { flexBasis = 10 }}); leftBox.style.flexGrow = 1; w.Add(leftBox); w.Add(new Label("R") { style = { flexBasis = 10 }}); rightBox.style.flexGrow = 1; w.Add(rightBox); } else { leftBox.style.flexGrow = 1; w.Add(leftBox); } return w; }, handProp); handSigns.style.flexGrow = 1; row.Add(handSigns); wrapper.Add(row); wrapper.Add(VRCFuryStateEditor.render(gesture.FindPropertyRelative("state"))); var disableBlinkProp = gesture.FindPropertyRelative("disableBlinking"); var customTransitionTimeProp = gesture.FindPropertyRelative("customTransitionTime"); var transitionTimeProp = gesture.FindPropertyRelative("transitionTime"); var enableLockMenuItemProp = gesture.FindPropertyRelative("enableLockMenuItem"); var lockMenuItemProp = gesture.FindPropertyRelative("lockMenuItem"); var enableExclusiveTagProp = gesture.FindPropertyRelative("enableExclusiveTag"); var exclusiveTagProp = gesture.FindPropertyRelative("exclusiveTag"); var enableWeightProp = gesture.FindPropertyRelative("enableWeight"); var button = VRCFuryEditorUtils.Button("Options", () => { var advMenu = new GenericMenu(); advMenu.AddItem(new GUIContent("Disable blinking when active"), disableBlinkProp.boolValue, () => { disableBlinkProp.boolValue = !disableBlinkProp.boolValue; gesture.serializedObject.ApplyModifiedProperties(); }); advMenu.AddItem(new GUIContent("Customize transition time"), customTransitionTimeProp.boolValue, () => { customTransitionTimeProp.boolValue = !customTransitionTimeProp.boolValue; gesture.serializedObject.ApplyModifiedProperties(); }); advMenu.AddItem(new GUIContent("Add 'Gesture Lock' toggle to menu"), enableLockMenuItemProp.boolValue, () => { enableLockMenuItemProp.boolValue = !enableLockMenuItemProp.boolValue; gesture.serializedObject.ApplyModifiedProperties(); }); advMenu.AddItem(new GUIContent("Enable exclusive tag"), enableExclusiveTagProp.boolValue, () => { enableExclusiveTagProp.boolValue = !enableExclusiveTagProp.boolValue; gesture.serializedObject.ApplyModifiedProperties(); }); advMenu.AddItem(new GUIContent("Use gesture weight (fist only)"), enableWeightProp.boolValue, () => { enableWeightProp.boolValue = !enableWeightProp.boolValue; gesture.serializedObject.ApplyModifiedProperties(); }); advMenu.ShowAsContext(); }); button.style.flexBasis = 70; row.Add(button); wrapper.Add(VRCFuryEditorUtils.RefreshOnChange(() => { var w = new VisualElement(); if (disableBlinkProp.boolValue) w.Add(VRCFuryEditorUtils.WrappedLabel("Blinking disabled when active")); if (customTransitionTimeProp.boolValue) w.Add(VRCFuryEditorUtils.Prop(transitionTimeProp, "Custom transition time (s)")); if (enableLockMenuItemProp.boolValue) w.Add(VRCFuryEditorUtils.Prop(lockMenuItemProp, "Lock menu item path")); if (enableExclusiveTagProp.boolValue) w.Add(VRCFuryEditorUtils.Prop(exclusiveTagProp, "Exclusive Tag")); if (enableWeightProp.boolValue) w.Add(VRCFuryEditorUtils.WrappedLabel("Use gesture weight (fist only)")); return w; }, disableBlinkProp, customTransitionTimeProp, enableLockMenuItemProp, enableExclusiveTagProp, enableWeightProp)); return wrapper; } public override bool AvailableOnProps() { return false; } } }