UI/FacePoser/FacePoseMorphRow.razor
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@namespace Sandbox
<root>
<div class="label @(IsActive ? "active" : "")">@Title</div>
<div class="sliders">
<SliderControl Property=@_so?.GetProperty(nameof(Strength)) ShowTextEntry=@(false) ShowValueTooltip=@(false)></SliderControl>
@if (IsPaired)
{
<SliderControl Property=@_so?.GetProperty(nameof(Side)) ShowTextEntry=@(false) ShowValueTooltip=@(false)></SliderControl>
}
</div>
</root>
@code
{
[Parameter] public SkinnedModelRenderer Target { get; set; }
[Parameter] public string NameA { get; set; }
[Parameter] public string NameB { get; set; }
[Parameter] public string Title { get; set; }
[Parameter] public Action<string, float> OnMorphChanged { get; set; }
bool IsPaired => NameB != null;
bool IsActive => (Target?.SceneModel?.Morphs.Get(NameA) ?? 0f) > 0f || (IsPaired && (Target?.SceneModel?.Morphs.Get(NameB) ?? 0f) > 0f);
SerializedObject _so;
// Live helpers — always read from the rendered mesh so sliders stay in sync
float LiveStrength()
{
if (!Target.IsValid()) return 0f;
var l = Target.SceneModel.Morphs.Get(NameA);
var r = IsPaired ? Target.SceneModel.Morphs.Get(NameB) : 0f;
return MathF.Max(l, r);
}
float LiveSide()
{
if (!Target.IsValid() || !IsPaired) return 0f;
var l = Target.SceneModel.Morphs.Get(NameA);
var r = Target.SceneModel.Morphs.Get(NameB);
if (r > l) return l == 0 ? -1f : -(1f - l / r);
if (l > r) return r == 0 ? 1f : 1f - r / l;
return 0f;
}
[Range(0f, 1f), Step(0.01f)]
public float Strength
{
get => LiveStrength();
set
{
if (!Target.IsValid()) return;
var side = LiveSide();
if (IsPaired)
{
var valA = value * side.Remap(0, -1, 1, 0).Clamp(0, 1);
var valB = value * side.Remap(0, 1, 1, 0).Clamp(0, 1);
Target.SceneModel.Morphs.Set(NameA, valA);
Target.SceneModel.Morphs.Set(NameB, valB);
OnMorphChanged?.Invoke(NameA, valA);
OnMorphChanged?.Invoke(NameB, valB);
}
else
{
Target.SceneModel.Morphs.Set(NameA, value);
OnMorphChanged?.Invoke(NameA, value);
}
}
}
[Range(-1f, 1f), Step(0.01f)]
public float Side
{
get => LiveSide();
set
{
if (!Target.IsValid() || !IsPaired) return;
var strength = LiveStrength();
var valA = strength * value.Remap(0, -1, 1, 0).Clamp(0, 1);
var valB = strength * value.Remap(0, 1, 1, 0).Clamp(0, 1);
Target.SceneModel.Morphs.Set(NameA, valA);
Target.SceneModel.Morphs.Set(NameB, valB);
OnMorphChanged?.Invoke(NameA, valA);
OnMorphChanged?.Invoke(NameB, valB);
}
}
protected override void OnParametersSet()
{
base.OnParametersSet();
_so ??= TypeLibrary.GetSerializedObject(this);
}
public void Clear()
{
if (!Target.IsValid()) return;
Target.Morphs.Clear(NameA);
if (IsPaired) Target.Morphs.Clear(NameB);
StateHasChanged();
}
protected override int BuildHash() => HashCode.Combine(Target, NameA, NameB);
}