#pragma once #include "CoreMinimal.h" #include "Player/Arm/DescentArmLayout.h" /** The named poses of the arm. Hidden = lowered out of the view; Idle = the relaxed ready stance on foot. */ enum class EDescentArmPose : uint8 { Hidden, Idle, Reach, Grip, Point, Plug, HoldTool, Count }; /** * Everything that moves the arm, as plain numbers (degrees, centimetres). A pose is a value: it can be blended, compared, * printed to the state dump, and turned into part transforms by ComputeLocal / ComputeWorld without any component or world. * * Sign conventions (Unreal axes, arm at rest along +X, palm down, thumb toward -Y): * ShoulderYawDeg + swings the arm to the right (+Y) ShoulderPitchDeg + raises the arm * ShoulderRollDeg + supination (thumb up) ElbowFlexDeg + folds the forearm up, 0 = straight * WristRollDeg + supination (thumb up) WristFlexDeg + bends the hand toward the palm * FingerCurl[f] 0 = finger straight, 1 = fist (the thumb: 0 = as modelled, 1 = tucked against the palm) * ShiftCm moves the whole arm (shoulder) in arm space, used by the hidden pose */ struct FDescentArmPose { double ShoulderYawDeg = 0.0; double ShoulderPitchDeg = 0.0; double ShoulderRollDeg = 0.0; double ElbowFlexDeg = 0.0; double WristRollDeg = 0.0; double WristFlexDeg = 0.0; /** Thumb, Index, Middle, Ring, Pinky. */ double FingerCurl[5] = { 0.0, 0.0, 0.0, 0.0, 0.0 }; FVector ShiftCm = FVector::ZeroVector; /** Component-wise interpolation, deterministic. Linear, except the shoulder's yaw and roll, which are circles and go the short way round. */ static FDescentArmPose Lerp(const FDescentArmPose& A, const FDescentArmPose& B, double Alpha); /** True when every number differs by at most Tolerance. */ bool Equals(const FDescentArmPose& Other, double Tolerance = 1e-9) const; /** The largest absolute difference of any joint angle in degrees / any curl x 100 / any shift in cm (a single "how far apart" figure). */ double MaxDifference(const FDescentArmPose& Other) const; }; using FDescentArmTransforms = TArray>; namespace Descent::Arm { THEDESCENT_API const TCHAR* PoseName(EDescentArmPose Pose); /** Case-insensitive; accepts "holdtool" and "hold_tool" and "hold-tool". */ THEDESCENT_API bool ParsePose(const FString& Name, EDescentArmPose& Out); /** The finished joints of a named pose (built once from the recipes in Core/Tuning/DescentTune_Arm.h with the reach solver). */ THEDESCENT_API const FDescentArmPose& NamedPose(EDescentArmPose Pose); // ---- joint maths (public so the solver and the tests share it) ---- THEDESCENT_API FQuat ShoulderRotation(double YawDeg, double PitchDeg, double RollDeg); THEDESCENT_API FQuat ElbowRotation(double FlexDeg); THEDESCENT_API FQuat WristRollRotation(double RollDeg); THEDESCENT_API FQuat WristFlexRotation(double FlexDeg); /** Rotation to ADD to a phalanx about its hinge, in degrees: how far this joint is from the pose the model was authored in. */ THEDESCENT_API double FingerJointDeltaDeg(int32 Finger, int32 Joint, double Curl); /** Absolute curl of one phalanx joint (0 = straight) for a finger curl; the thumb's is relative to its modelled pose. */ THEDESCENT_API double FingerJointAbsoluteDeg(int32 Finger, int32 Joint, double Curl); /** Piston rod travel along its axis for an elbow flexion: 0 when straight, negative (retracting) as the elbow bends. */ THEDESCENT_API double RodSlideCm(double ElbowFlexDeg); /** Transform of every part relative to its parent (cm), in table order. */ THEDESCENT_API void ComputeLocal(const FDescentArmPose& Pose, FDescentArmTransforms& Out); /** Transform of every part in arm space (cm): origin = the shoulder (plus ShiftCm), +X along the arm. */ THEDESCENT_API void ComputeWorld(const FDescentArmPose& Pose, FDescentArmTransforms& Out); } /** * Smooth blending between poses. The result is a pure function of the calls made, never of the frame rate: * BlendTo() snapshots the current pose as the start, Advance(dt) moves a smoothstep from it to the target over the duration. */ class THEDESCENT_API FDescentArmBlender { public: void Reset(const FDescentArmPose& Pose); /** Starts a blend from wherever the arm is now. A duration of 0 or less snaps. */ void BlendTo(const FDescentArmPose& Target, double DurationSeconds); void Advance(double DeltaSeconds); const FDescentArmPose& Current() const { return Cur; } const FDescentArmPose& TargetPose() const { return To; } /** Linear progress 0..1 of the running blend (1 when idle). */ double Progress() const { return Duration <= 0.0 ? 1.0 : FMath::Clamp(Elapsed / Duration, 0.0, 1.0); } /** The smoothstep of Progress(): the weight of the target pose actually applied. */ double Weight() const; bool IsBlending() const { return Duration > 0.0 && Elapsed < Duration; } private: FDescentArmPose From, To, Cur; double Duration = 0.0; double Elapsed = 0.0; };