Files
XplorePlane/XP.Common/Controls/JoystickCalculator.cs
T
2026-04-22 20:48:39 +08:00

88 lines
4.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
namespace XP.Common.Controls
{
/// <summary>
/// 虚拟摇杆核心计算逻辑(纯函数,无副作用)| Virtual joystick core calculation logic (pure functions, no side effects)
/// </summary>
public static class JoystickCalculator
{
/// <summary>
/// 将位移限制在圆形区域内 | Clamp displacement within circular radius
/// </summary>
/// <param name="dx">X 轴偏移量 | X axis displacement</param>
/// <param name="dy">Y 轴偏移量 | Y axis displacement</param>
/// <param name="radius">最大半径(必须大于 0| Maximum radius (must be greater than 0)</param>
/// <returns>限制后的 (clampedDx, clampedDy) 元组 | Clamped (clampedDx, clampedDy) tuple</returns>
public static (double clampedDx, double clampedDy) ClampToRadius(double dx, double dy, double radius)
{
if (radius <= 0)
throw new ArgumentOutOfRangeException(nameof(radius), "半径必须大于 0 | Radius must be greater than 0");
var distance = Math.Sqrt(dx * dx + dy * dy);
// 如果距离在半径内,直接返回原值 | If within radius, return original values
if (distance <= radius)
return (dx, dy);
// 归一化到半径上 | Normalize to radius boundary
var scale = radius / distance;
return (dx * scale, dy * scale);
}
/// <summary>
/// 计算归一化输出,包含死区映射 | Calculate normalized output with dead zone mapping
/// </summary>
/// <param name="dx">X 轴偏移量 | X axis displacement</param>
/// <param name="dy">Y 轴偏移量 | Y axis displacement</param>
/// <param name="radius">最大半径(必须大于 0| Maximum radius (must be greater than 0)</param>
/// <param name="deadZone">死区比例,范围 [0.0, 1.0) | Dead zone ratio, range [0.0, 1.0)</param>
/// <param name="mode">摇杆模式 | Joystick mode</param>
/// <returns>归一化输出 (outputX, outputY) 元组,范围 [-1.0, 1.0] | Normalized output tuple, range [-1.0, 1.0]</returns>
public static (double outputX, double outputY) CalculateOutput(
double dx, double dy, double radius, double deadZone, JoystickMode mode)
{
if (radius <= 0)
throw new ArgumentOutOfRangeException(nameof(radius), "半径必须大于 0 | Radius must be greater than 0");
if (deadZone < 0.0 || deadZone >= 1.0)
throw new ArgumentOutOfRangeException(nameof(deadZone), "死区比例必须在 [0.0, 1.0) 范围内 | Dead zone must be in range [0.0, 1.0)");
// 步骤 1-2:将位移限制在半径内 | Step 1-2: Clamp displacement within radius
var (clampedDx, clampedDy) = ClampToRadius(dx, dy, radius);
// 步骤 3:计算比例值 | Step 3: Calculate ratio
var ratioX = clampedDx / radius;
var ratioY = clampedDy / radius;
// 步骤 4:对每个轴分别应用死区 | Step 4: Apply dead zone to each axis independently
var outputX = ApplyDeadZone(ratioX, deadZone);
var outputY = ApplyDeadZone(ratioY, deadZone);
// 步骤 5SingleAxisY 模式下强制 OutputX = 0 | Step 5: Force OutputX = 0 in SingleAxisY mode
if (mode == JoystickMode.SingleAxisY)
outputX = 0.0;
return (outputX, outputY);
}
/// <summary>
/// 对单轴比例值应用死区映射 | Apply dead zone mapping to a single axis ratio
/// </summary>
/// <param name="ratio">轴比例值,范围 [-1.0, 1.0] | Axis ratio, range [-1.0, 1.0]</param>
/// <param name="deadZone">死区比例,范围 [0.0, 1.0) | Dead zone ratio, range [0.0, 1.0)</param>
/// <returns>死区映射后的输出值 | Output value after dead zone mapping</returns>
internal static double ApplyDeadZone(double ratio, double deadZone)
{
var absRatio = Math.Abs(ratio);
// 在死区内,输出为 0 | Within dead zone, output is 0
if (absRatio < deadZone)
return 0.0;
// 死区外线性映射:sign(ratio) × (absRatio - D) / (1 - D) | Linear mapping outside dead zone
return Math.Sign(ratio) * (absRatio - deadZone) / (1.0 - deadZone);
}
}
}