feat: Menu Item automatic values (#1098)

This commit is contained in:
bd_ 2024-09-03 19:07:33 -07:00 committed by GitHub
parent c63128095e
commit 0ee291076f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 255 additions and 49 deletions

View File

@ -54,6 +54,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 +106,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);
}
@ -180,6 +182,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;
@ -225,7 +228,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);
@ -462,6 +465,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (knownParameter != null && knownParameter.Source is ModularAvatarMenuItem)
isDefaultByKnownParam = null;
if (_prop_automaticValue?.boolValue == true) isDefaultByKnownParam = null;
Object controller = knownParameter?.Source;
// If we can't figure out what to reference the parameter names to, or if they're controlled by something
@ -549,6 +554,55 @@ 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<ModularAvatarMenuItem> FindSiblingMenuItems(SerializedObject serializedObject)
{
if (serializedObject == null || serializedObject.isEditingMultipleObjects) return null;

View File

@ -187,6 +187,8 @@
"menuitem.prop.type.tooltip": "The type of this item",
"menuitem.prop.value": "Value",
"menuitem.prop.value.tooltip": "The value to set the parameter to when this control is used",
"menuitem.prop.automatic_value": "Auto",
"menuitem.prop.automatic_value.tooltip": "Automatically set this control to a unique value",
"menuitem.prop.parameter": "Parameter",
"menuitem.prop.label": "Label",
"menuitem.prop.submenu_asset": "Submenu Asset",

View File

@ -197,6 +197,8 @@
"menuitem.prop.is_saved.tooltip": "有効になっていると、アバター変更やワールド移動するときこの設定が保持されます。",
"menuitem.prop.is_synced": "同期する",
"menuitem.prop.is_synced.tooltip": "有効の場合はほかのプレイヤーに同期されます。",
"menuitem.prop.automatic_value": "自動",
"menuitem.prop.automatic_value.tooltip": "かぶらない値を自動的に割り振る",
"menuitem.param.rotation": "回転パラメーター名",
"menuitem.param.rotation.tooltip": "このメニューアイテムの回転に連動するべきパラメーター",
"menuitem.param.horizontal": "横パラメーター名",

View File

@ -63,6 +63,17 @@ namespace nadena.dev.modular_avatar.core.editor
_computeContext.Observe(mami, c => (c.Control?.parameter, c.Control?.type, c.Control?.value, c.isDefault));
var mami_condition = ParameterAssignerPass.AssignMenuItemParameter(mami, _simulationInitialStates);
if (mami_condition != null &&
ForceMenuItems.TryGetValue(mami_condition.Parameter, out var forcedMenuItem))
{
var enable = forcedMenuItem == mami;
mami_condition.InitialValue = 0.5f;
mami_condition.ParameterValueLo = enable ? 0 : 999f;
mami_condition.ParameterValueHi = 1000;
mami_condition.IsConstant = true;
}
if (mami_condition != null) conditions.Add(mami_condition);
}

View File

@ -21,6 +21,9 @@ namespace nadena.dev.modular_avatar.core.editor
public ImmutableDictionary<string, float> ForcePropertyOverrides { get; set; } = ImmutableDictionary<string, float>.Empty;
public ImmutableDictionary<string, ModularAvatarMenuItem> ForceMenuItems { get; set; } =
ImmutableDictionary<string, ModularAvatarMenuItem>.Empty;
public ReactiveObjectAnalyzer(ndmf.BuildContext context)
{
_computeContext = ComputeContext.NullContext;
@ -47,7 +50,8 @@ namespace nadena.dev.modular_avatar.core.editor
var mami = obj?.GetComponent<ModularAvatarMenuItem>();
if (mami == null) return null;
return ParameterAssignerPass.AssignMenuItemParameter(mami, _simulationInitialStates)?.Parameter;
return ParameterAssignerPass.AssignMenuItemParameter(mami, _simulationInitialStates, ForceMenuItems)
?.Parameter;
}
public struct AnalysisResult
@ -68,6 +72,8 @@ namespace nadena.dev.modular_avatar.core.editor
var analysis = new ReactiveObjectAnalyzer(ctx);
analysis.ForcePropertyOverrides = ctx.Observe(ROSimulator.PropertyOverrides, a=>a, (a,b) => false)
?? ImmutableDictionary<string, float>.Empty;
analysis.ForceMenuItems = ctx.Observe(ROSimulator.MenuItemOverrides, a => a, (a, b) => false)
?? ImmutableDictionary<string, ModularAvatarMenuItem>.Empty;
return analysis.Analyze(root);
});
}

View File

@ -75,7 +75,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (condition.IsConstant) continue;
if (!initialValues.ContainsKey(condition.Parameter))
if (!initialValues.ContainsKey(condition.Parameter) && condition.InitialValue > -999f)
initialValues[condition.Parameter] = condition.InitialValue;
}
}

View File

@ -47,11 +47,14 @@ namespace nadena.dev.modular_avatar.core.editor
var paramIndex = 0;
var declaredParams = context.AvatarDescriptor.expressionParameters.parameters.Select(p => p.name)
.ToHashSet();
var declaredParams = context.AvatarDescriptor.expressionParameters.parameters
.GroupBy(p => p.name).Select(l => l.First())
.ToDictionary(p => p.name);
Dictionary<string, VRCExpressionParameters.Parameter> newParameters = new();
Dictionary<string, int> nextParamValue = new();
Dictionary<string, List<ModularAvatarMenuItem>> _mamiByParam = new();
foreach (var mami in context.AvatarRootTransform.GetComponentsInChildren<ModularAvatarMenuItem>(true))
{
if (string.IsNullOrWhiteSpace(mami.Control?.parameter?.name))
@ -67,48 +70,94 @@ namespace nadena.dev.modular_avatar.core.editor
var paramName = mami.Control.parameter.name;
if (!declaredParams.Contains(paramName))
if (!_mamiByParam.TryGetValue(paramName, out var mamiList))
{
newParameters.TryGetValue(paramName, out var existingNewParam);
var wantedType = existingNewParam?.valueType ?? VRCExpressionParameters.ValueType.Bool;
mamiList = new List<ModularAvatarMenuItem>();
_mamiByParam[paramName] = mamiList;
}
if (wantedType != VRCExpressionParameters.ValueType.Float &&
(mami.Control.value > 1.01 || mami.Control.value < -0.01))
wantedType = VRCExpressionParameters.ValueType.Int;
mamiList.Add(mami);
}
if (Mathf.Abs(Mathf.Round(mami.Control.value) - mami.Control.value) > 0.01f)
wantedType = VRCExpressionParameters.ValueType.Float;
if (existingNewParam == null)
foreach (var (paramName, list) in _mamiByParam)
{
existingNewParam = new VRCExpressionParameters.Parameter
// Assign automatic values first
float defaultValue;
if (declaredParams.TryGetValue(paramName, out var p))
{
name = paramName,
valueType = wantedType,
saved = mami.isSaved,
defaultValue = -1,
networkSynced = mami.isSynced
};
newParameters[paramName] = existingNewParam;
defaultValue = p.defaultValue;
}
else
{
existingNewParam.valueType = wantedType;
defaultValue = list.FirstOrDefault(m => m.isDefault && !m.automaticValue)?.Control?.value ?? 0;
if (list.Count == 1)
// If we have only a single entry, it's probably an on-off toggle, so we'll implicitly let 0
// be the 'unselected' default value
defaultValue = 1;
}
// TODO: warn on inconsistent configuration
existingNewParam.saved = existingNewParam.saved || mami.isSaved;
existingNewParam.networkSynced = existingNewParam.networkSynced || mami.isSynced;
existingNewParam.defaultValue = mami.isDefault ? mami.Control.value : existingNewParam.defaultValue;
HashSet<int> usedValues = new();
usedValues.Add((int)defaultValue);
foreach (var item in list)
if (!item.automaticValue && Mathf.Abs(item.Control.value - Mathf.Round(item.Control.value)) < 0.01f)
usedValues.Add(Mathf.RoundToInt(item.Control.value));
var nextValue = 1;
var canBeBool = true;
var canBeInt = true;
var isSaved = true;
var isSynced = true;
foreach (var mami in list)
{
if (mami.automaticValue)
{
if (mami.isDefault)
{
mami.Control.value = defaultValue;
}
else
{
while (usedValues.Contains(nextValue)) nextValue++;
mami.Control.value = nextValue;
usedValues.Add(nextValue);
}
}
if (Mathf.Abs(mami.Control.value - Mathf.Round(mami.Control.value)) > 0.01f)
canBeInt = false;
else
canBeBool &= mami.Control.value is >= 0 and <= 1;
isSaved &= mami.isSaved;
isSynced &= mami.isSynced;
}
if (!declaredParams.ContainsKey(paramName))
{
VRCExpressionParameters.ValueType newType;
if (canBeBool) newType = VRCExpressionParameters.ValueType.Bool;
else if (canBeInt) newType = VRCExpressionParameters.ValueType.Int;
else newType = VRCExpressionParameters.ValueType.Float;
var newParam = new VRCExpressionParameters.Parameter
{
name = paramName,
valueType = newType,
saved = isSaved,
defaultValue = defaultValue,
networkSynced = isSynced
};
newParameters[paramName] = newParam;
}
}
if (newParameters.Count > 0)
{
foreach (var p in newParameters)
if (p.Value.defaultValue < 0)
p.Value.defaultValue = 0;
var expParams = context.AvatarDescriptor.expressionParameters;
if (!context.IsTemporaryAsset(expParams))
{
@ -120,15 +169,22 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
internal static ControlCondition AssignMenuItemParameter(ModularAvatarMenuItem mami, Dictionary<string, float> simulationInitialStates = null)
internal static ControlCondition AssignMenuItemParameter(
ModularAvatarMenuItem mami,
Dictionary<string, float> simulationInitialStates = null,
IDictionary<string, ModularAvatarMenuItem> isDefaultOverrides = null)
{
var paramName = mami?.Control?.parameter?.name;
if (mami?.Control != null && simulationInitialStates != null && ShouldAssignParametersToMami(mami))
{
paramName = "___AutoProp/" + mami.Control?.parameter?.name;
if (paramName == "___AutoProp/") paramName += mami.GetInstanceID();
paramName = mami.Control?.parameter?.name;
if (string.IsNullOrEmpty(paramName)) paramName = "___AutoProp/" + mami.GetInstanceID();
if (mami.isDefault)
var isDefault = mami.isDefault;
if (isDefaultOverrides?.TryGetValue(paramName, out var target) == true)
isDefault = ReferenceEquals(mami, target);
if (isDefault)
{
simulationInitialStates[paramName] = mami.Control.value;
} else if (!simulationInitialStates.ContainsKey(paramName))
@ -144,7 +200,10 @@ namespace nadena.dev.modular_avatar.core.editor
Parameter = paramName,
DebugName = mami.gameObject.name,
IsConstant = false,
InitialValue = mami.isDefault ? mami.Control.value : -999, // TODO
// Note: This slightly odd-looking value is key to making the Auto checkbox work for editor previews;
// we basically force-disable any conditions for nonselected menu items and force-enable any for default
// menu items.
InitialValue = mami.isDefault ? mami.Control.value : -999,
ParameterValueLo = mami.Control.value - 0.5f,
ParameterValueHi = mami.Control.value + 0.5f,
DebugReference = mami,

View File

@ -15,6 +15,7 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
internal class ROSimulator : EditorWindow, IHasCustomMenu
{
public static PublishedValue<ImmutableDictionary<string, float>> PropertyOverrides = new(null);
public static PublishedValue<ImmutableDictionary<string, ModularAvatarMenuItem>> MenuItemOverrides = new(null);
internal static string ROOT_PATH = "Packages/nadena.dev.modular-avatar/Editor/ReactiveObjects/Simulator/";
private static string USS = ROOT_PATH + "ROSimulator.uss";
@ -64,6 +65,7 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
private void OnEnable()
{
PropertyOverrides.Value = ImmutableDictionary<string, float>.Empty;
MenuItemOverrides.Value = ImmutableDictionary<string, ModularAvatarMenuItem>.Empty;
EditorApplication.delayCall += LoadUI;
Selection.selectionChanged += SelectionChanged;
is_enabled = true;
@ -79,6 +81,7 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
EditorApplication.delayCall += () =>
{
PropertyOverrides.Value = null;
MenuItemOverrides.Value = null;
};
}
@ -107,6 +110,25 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
EditorApplication.delayCall += RefreshUI;
}
private void UpdateMenuItemOverride(string prop, ModularAvatarMenuItem item, bool? value)
{
if (value == null)
{
MenuItemOverrides.Value = MenuItemOverrides.Value.Remove(prop);
}
else if (value.Value)
{
MenuItemOverrides.Value = MenuItemOverrides.Value.SetItem(prop, item);
}
else
{
if (MenuItemOverrides.Value.TryGetValue(prop, out var existing) && ReferenceEquals(existing, item))
MenuItemOverrides.Value = MenuItemOverrides.Value.SetItem(prop, null);
}
EditorApplication.delayCall += RefreshUI;
}
private void UpdatePropertyOverride(string prop, bool? value)
{
if (value == null)
@ -209,6 +231,7 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
var analysis = new ReactiveObjectAnalyzer(_lastComputeContext);
analysis.ForcePropertyOverrides = PropertyOverrides.Value;
analysis.ForceMenuItems = MenuItemOverrides.Value;
var result = analysis.Analyze(avatar.gameObject);
SetThisObjectOverrides(analysis);
@ -227,9 +250,36 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
private void SetThisObjectOverrides(ReactiveObjectAnalyzer analysis)
{
BindOverrideToParameter("this-obj-override", analysis.GetGameObjectStateProperty(currentSelection), 1);
BindOverrideToParameter("this-menu-override", analysis.GetMenuItemProperty(currentSelection),
currentSelection.GetComponent<ModularAvatarMenuItem>()?.Control?.value ?? 1
);
currentSelection.TryGetComponent<ModularAvatarMenuItem>(out var mami);
BindOverrideToMenuItem("this-menu-override", mami);
}
private void BindOverrideToMenuItem(string overrideElemName, ModularAvatarMenuItem mami)
{
var elem = e_debugInfo.Q<VisualElement>(overrideElemName);
var soc = elem.Q<StateOverrideController>();
if (mami == null)
{
elem.style.display = DisplayStyle.None;
return;
}
var prop = ParameterAssignerPass.AssignMenuItemParameter(mami)?.Parameter;
if (prop == null)
{
elem.style.display = DisplayStyle.None;
return;
}
elem.style.display = DisplayStyle.Flex;
if (MenuItemOverrides.Value.TryGetValue(prop, out var overrideValue))
soc.SetWithoutNotify(ReferenceEquals(mami, overrideValue));
else
soc.SetWithoutNotify(null);
soc.OnStateOverrideChanged += value => { UpdateMenuItemOverride(prop, mami, value); };
}
private void BindOverrideToParameter(string overrideElemName, string property, float targetValue)
@ -461,7 +511,22 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
targetValue = Mathf.Round((condition.ParameterValueLo + condition.ParameterValueHi) / 2);
}
if (condition.DebugReference is ModularAvatarMenuItem mami)
{
bool? menuOverride = null;
if (MenuItemOverrides.Value.TryGetValue(prop, out var target))
{
menuOverride = ReferenceEquals(mami, target);
soc.SetWithoutNotify(menuOverride);
}
soc.OnStateOverrideChanged += value => { UpdateMenuItemOverride(prop, mami, value); };
}
else
{
soc.OnStateOverrideChanged += value => UpdatePropertyOverride(prop, value, targetValue);
}
var active = condition.InitiallyActive;
var active_label = active ? "active" : "inactive";

View File

@ -1,6 +1,5 @@
#if MA_VRCSDK3_AVATARS
using System;
using System.Linq;
using nadena.dev.modular_avatar.core.menu;
using UnityEngine;
@ -43,6 +42,13 @@ namespace nadena.dev.modular_avatar.core
/// </summary>
public bool isDefault;
/// <summary>
/// If true, the value for this toggle or button menu item will be automatically selected.
/// Typically, this will be zero for the default menu item, then subsequent menu items will be allocated
/// sequentially in hierarchy order.
/// </summary>
public bool automaticValue;
private void Reset()
{
Control = new VRCExpressionsMenu.Control();
@ -51,6 +57,7 @@ namespace nadena.dev.modular_avatar.core
isSaved = true;
isSynced = true;
isDefault = false;
automaticValue = true;
MenuSource = SubmenuSource.Children;
}