312 lines
16 KiB
C#
312 lines
16 KiB
C#
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<GestureDriver> {
|
|
private int i = 0;
|
|
private readonly Dictionary<string, VFABool> lockMenuItems = new Dictionary<string, VFABool>();
|
|
private readonly Dictionary<string, VFACondition> excludeConditions = new Dictionary<string, VFACondition>();
|
|
|
|
[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;
|
|
}
|
|
}
|
|
} |