Code/Dynamics/RotationDynamics.cs
using System;

namespace Andicraft.SecondOrderDynamics;

public class RotationDynamics : SecondOrderDynamics<Rotation>
{
	/// <inheritdoc/>
	public RotationDynamics(Rotation startingValue) : base(startingValue)
	{
	}

	/// <inheritdoc/>
	public RotationDynamics(Vector3 parameters, Rotation startingValue) : base(parameters, startingValue)
	{
	}

	/// <inheritdoc/>
	public RotationDynamics(float frequency, float damping, float response, Rotation startingValue) : base(frequency, damping, response, startingValue)
	{
	}

	/// <inheritdoc/>
	public override void Reset(Rotation value)
	{
		_previousValue = value;
		_currentValue = value;
		_velocity = Rotation.Identity;
	}

	/// <inheritdoc/>
	public override Rotation Update(float deltaTime, Rotation target, bool setVelocity = false,
		Rotation velocity = default)
	{
		NormalizeSign(_currentValue, ref target);

		if (setVelocity == false)
		{
			velocity.x = (target.x - _previousValue.x) / deltaTime;
			velocity.y = (target.y - _previousValue.y) / deltaTime;
			velocity.z = (target.z - _previousValue.z) / deltaTime;
			velocity.w = (target.w - _previousValue.w) / deltaTime;
			_previousValue = target;
		}

		var k2Stable = MathF.Max(k2, MathF.Max(deltaTime * deltaTime / 2 + deltaTime * k1 / 2, deltaTime * k1));

		_currentValue.x += deltaTime * _velocity.x;
		_currentValue.y += deltaTime * _velocity.y;
		_currentValue.z += deltaTime * _velocity.z;
		_currentValue.w += deltaTime * _velocity.w;

		_velocity.x += deltaTime * (target.x + k3 * velocity.x - _currentValue.x - k1 * _velocity.x) / k2Stable;
		_velocity.y += deltaTime * (target.y + k3 * velocity.y - _currentValue.y - k1 * _velocity.y) / k2Stable;
		_velocity.z += deltaTime * (target.z + k3 * velocity.z - _currentValue.z - k1 * _velocity.z) / k2Stable;
		_velocity.w += deltaTime * (target.w + k3 * velocity.w - _currentValue.w - k1 * _velocity.w) / k2Stable;

		return _currentValue.Normal;
	}

	private static void NormalizeSign(Rotation current, ref Rotation target)
	{
		// if our dot product is positive, we don't need to invert signs.
		if (current.Dot(target) >= 0) return;

		// invert the signs on the components
		target.x *= -1;
		target.y *= -1;
		target.z *= -1;
		target.w *= -1;
	}
}