HumanoidMocap/Core/Motion/WristPositionOffsets.cs

Static helper for wrist position corrections. It validates a list of WristPositionOffset entries, computes a per-edit weight given time and clip boundaries using a smoothstep fade, and samples the combined weighted offset for a given hand and time.

Reflection
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using HumanoidMocap.Core.Mapping;

namespace HumanoidMocap.Core.Motion;
using Vector3 = System.Numerics.Vector3;

public static class WristPositionOffsets
{
    public static void Validate(IReadOnlyList<WristPositionOffset> edits)
    {
        if(edits is null||edits.Count>64)throw new ArgumentException("At most 64 wrist corrections are supported.");
        foreach(var edit in edits)
            if(edit is null||edit.Hand is not (BoneRole.HandL or BoneRole.HandR)||!double.IsFinite(edit.Start)||!double.IsFinite(edit.End)||
                edit.End<=edit.Start||!double.IsFinite(edit.FadeSeconds)||edit.FadeSeconds<=0||edit.FadeSeconds>5||
                edit.Offset is not {Length:3}||edit.Offset.Any(v=>!float.IsFinite(v))||MotionDocument.V(edit.Offset).LengthSquared()>.25f)
                throw new ArgumentException("Wrist corrections need a hand, a nonempty time range, a fade in (0,5] seconds and a finite offset no longer than 50 cm.");
        for(var i=0;i<edits.Count;i++)for(var j=i+1;j<edits.Count;j++)
            if(edits[i].Enabled&&edits[j].Enabled&&edits[i].Hand==edits[j].Hand&&edits[i].Start<edits[j].End&&edits[j].Start<edits[i].End)
                throw new ArgumentException("Enabled corrections for the same wrist cannot overlap. Edit or disable the existing interval first.");
    }

    public static float Weight(WristPositionOffset edit,double time,double clipStart,double clipEnd)
    {
        if(!edit.Enabled||time<edit.Start||time>edit.End)return 0;
        var fade=Math.Min(edit.FadeSeconds,(edit.End-edit.Start)/2);
        double Smooth(double t){t=Math.Clamp(t,0,1);return t*t*(3-2*t);}
        // Clip boundaries have no neighbouring samples to blend to. Interior
        // boundaries return smoothly to untouched motion without a position snap.
        var enter=edit.Start<=clipStart?1:Smooth((time-edit.Start)/fade);
        var leave=edit.End>=clipEnd?1:Smooth((edit.End-time)/fade);
        return (float)Math.Min(enter,leave);
    }

    public static Vector3 Sample(IReadOnlyList<WristPositionOffset> edits,BoneRole hand,double time,double clipStart,double clipEnd)
    {
        var offset=Vector3.Zero;
        foreach(var edit in edits)if(edit.Hand==hand)offset+=MotionDocument.V(edit.Offset)*Weight(edit,time,clipStart,clipEnd);
        return offset;
    }
}