using System;
using System.Numerics;
using DatReaderWriter.Types;
namespace AcDream.Core.Physics.Motion;
///
/// R1-P3 — verbatim ports of retail's Frame rotation helpers used by
/// the CSequence physics path (oracle: raw decomp reads 2026-07-02).
///
///
/// - Frame::grotate (0x005357a0, pc:319782): build the
/// axis-angle quaternion from a GLOBAL rotation vector (angle = |v|,
/// axis = v/|v|, half-angle sin/cos) and PREMULTIPLY it onto the frame's
/// orientation — the incremental rotation is applied in world space.
/// Rotations with |v|² < F_EPSILON² are skipped.
/// - Frame::rotate (0x004525b0, pc:91477): map the LOCAL
/// rotation vector through the frame's local→global rotation
/// (m_fl2gv — our quaternion), then grotate.
///
///
public static class FrameOps
{
/// Retail F_EPSILON (0.000199999995f); grotate gates on its
/// square against |v|².
public const float FEpsilon = 0.000199999995f;
///
/// Retail Frame::set_rotate (0x00535080). The candidate is
/// normalized in extended precision, then the complete frame is checked
/// by Frame::IsValid (0x00534ED0). If that check fails, retail
/// restores the previous quaternion instead of allowing NaNs to poison
/// the live object transform.
///
public static Quaternion SetRotate(
Vector3 frameOrigin,
Quaternion previous,
Quaternion candidate)
{
double lengthSquared =
((double)candidate.W * candidate.W)
+ ((double)candidate.X * candidate.X)
+ ((double)candidate.Y * candidate.Y)
+ ((double)candidate.Z * candidate.Z);
double inverseLength = 1.0 / Math.Sqrt(lengthSquared);
var normalized = new Quaternion(
(float)(candidate.X * inverseLength),
(float)(candidate.Y * inverseLength),
(float)(candidate.Z * inverseLength),
(float)(candidate.W * inverseLength));
if (float.IsNaN(frameOrigin.X)
|| float.IsNaN(frameOrigin.Y)
|| float.IsNaN(frameOrigin.Z)
|| float.IsNaN(normalized.W)
|| float.IsNaN(normalized.X)
|| float.IsNaN(normalized.Y)
|| float.IsNaN(normalized.Z))
{
return previous;
}
float normalizedLengthSquared = normalized.LengthSquared();
return !float.IsNaN(normalizedLengthSquared)
&& MathF.Abs(normalizedLengthSquared - 1f) < FEpsilon * 5f
? normalized
: previous;
}
/// Frame::grotate — incremental WORLD-space rotation.
public static void GRotate(Frame frame, Vector3 rotationGlobal)
{
float magSq = rotationGlobal.LengthSquared();
if (magSq < FEpsilon * FEpsilon)
return;
float angle = MathF.Sqrt(magSq);
float invMag = 1f / angle;
float half = angle * 0.5f;
float s = MathF.Sin(half);
float c = MathF.Cos(half);
// Retail's set_rotate receives the raw Hamilton product r ⊗ q
// (r = the new axis-angle quat, q = the current orientation) —
// rotation applied in GLOBAL space. System.Numerics
// Quaternion.Multiply(r, q) is that product with
// Transform(v, r*q) == r-applied-after-q. set_rotate renormalizes.
var r = new Quaternion(
rotationGlobal.X * s * invMag,
rotationGlobal.Y * s * invMag,
rotationGlobal.Z * s * invMag,
c);
frame.Orientation = SetRotate(
frame.Origin,
frame.Orientation,
Quaternion.Multiply(r, frame.Orientation));
}
/// Frame::rotate — LOCAL rotation vector, mapped to
/// global through the orientation, then .
public static void Rotate(Frame frame, Vector3 rotationLocal)
=> GRotate(frame, Vector3.Transform(rotationLocal, frame.Orientation));
///
/// Frame::combine as used by the CSequence pose path (ACE
/// AFrame.Combine, verified against the pre-R1 acdream port):
/// apply the pose — origin += rotate(pos.Origin by orientation), then
/// orientation ∘= pos.Orientation.
///
public static void Combine(Frame frame, Frame pos)
{
frame.Origin += Vector3.Transform(pos.Origin, frame.Orientation);
frame.Orientation = SetRotate(
frame.Origin,
frame.Orientation,
frame.Orientation * pos.Orientation);
}
///
/// Frame::subtract1 — un-apply the pose: orientation ∘=
/// conj(pos.Orientation) FIRST, then origin −= rotate(pos.Origin by the
/// UPDATED orientation) (inverse order of ).
///
public static void Subtract1(Frame frame, Frame pos)
{
frame.Orientation = SetRotate(
frame.Origin,
frame.Orientation,
frame.Orientation * Quaternion.Conjugate(pos.Orientation));
frame.Origin -= Vector3.Transform(pos.Origin, frame.Orientation);
}
}