use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InputGroup {
ProjectileDrag,
MuzzleVelocity,
ZeroSightGeometry,
Atmosphere,
Wind,
ShotGeometry,
Effects,
}
impl InputGroup {
pub const ALL: &'static [InputGroup] = &[
InputGroup::ProjectileDrag,
InputGroup::MuzzleVelocity,
InputGroup::ZeroSightGeometry,
InputGroup::Atmosphere,
InputGroup::Wind,
InputGroup::ShotGeometry,
InputGroup::Effects,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InputAxis {
Mass,
Diameter,
Length,
BallisticCoefficient,
TwistRate,
TwistDirection,
DragModel,
MuzzleVelocityMps,
SightHeight,
ZeroDistance,
ZeroPoiUp,
ZeroPoiRight,
SightOffsetLateral,
MuzzleHeight,
MuzzleAngle,
Altitude,
Temperature,
Pressure,
RelativeHumidity,
Latitude,
WindSpeed,
WindDirection,
WindVertical,
TargetDistance,
ShootingAngle,
Cant,
ShotAzimuth,
AimAzimuth,
TargetHeight,
MagnusEnabled,
CoriolisEnabled,
EnhancedSpinDriftEnabled,
}
impl InputAxis {
pub const ALL: &'static [InputAxis] = &[
InputAxis::Mass,
InputAxis::Diameter,
InputAxis::Length,
InputAxis::BallisticCoefficient,
InputAxis::TwistRate,
InputAxis::TwistDirection,
InputAxis::DragModel,
InputAxis::MuzzleVelocityMps,
InputAxis::SightHeight,
InputAxis::ZeroDistance,
InputAxis::ZeroPoiUp,
InputAxis::ZeroPoiRight,
InputAxis::SightOffsetLateral,
InputAxis::MuzzleHeight,
InputAxis::MuzzleAngle,
InputAxis::Altitude,
InputAxis::Temperature,
InputAxis::Pressure,
InputAxis::RelativeHumidity,
InputAxis::Latitude,
InputAxis::WindSpeed,
InputAxis::WindDirection,
InputAxis::WindVertical,
InputAxis::TargetDistance,
InputAxis::ShootingAngle,
InputAxis::Cant,
InputAxis::ShotAzimuth,
InputAxis::AimAzimuth,
InputAxis::TargetHeight,
InputAxis::MagnusEnabled,
InputAxis::CoriolisEnabled,
InputAxis::EnhancedSpinDriftEnabled,
];
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AxisKind {
Continuous {
unit: &'static str,
default_rel_step: f64,
min_abs_step: f64,
},
Categorical,
}
#[derive(Debug, Clone, Copy)]
pub struct AxisMeta {
pub group: InputGroup,
pub kind: AxisKind,
pub requires_rezero: bool,
}
const fn cont(unit: &'static str, rel: f64, min_abs: f64) -> AxisKind {
AxisKind::Continuous {
unit,
default_rel_step: rel,
min_abs_step: min_abs,
}
}
pub fn axis_meta(axis: InputAxis) -> AxisMeta {
use InputAxis::*;
use InputGroup::*;
let (group, kind, requires_rezero) = match axis {
Mass => (ProjectileDrag, cont("kg", 1e-3, 1e-7), true),
Diameter => (ProjectileDrag, cont("m", 1e-3, 1e-7), true),
Length => (ProjectileDrag, cont("m", 1e-3, 1e-6), false),
BallisticCoefficient => (ProjectileDrag, cont("", 1e-3, 1e-4), true),
TwistRate => (ProjectileDrag, cont("m/turn", 1e-3, 1e-5), false),
TwistDirection => (ProjectileDrag, AxisKind::Categorical, false),
DragModel => (ProjectileDrag, AxisKind::Categorical, true),
MuzzleVelocityMps => (MuzzleVelocity, cont("m/s", 1e-3, 0.15), true),
SightHeight => (ZeroSightGeometry, cont("m", 1e-3, 1e-5), true),
ZeroDistance => (ZeroSightGeometry, cont("m", 1e-3, 0.1), true),
ZeroPoiUp => (ZeroSightGeometry, cont("m", 1e-3, 1e-5), true),
ZeroPoiRight => (ZeroSightGeometry, cont("m", 1e-3, 1e-5), true),
SightOffsetLateral => (ZeroSightGeometry, cont("m", 1e-3, 1e-5), true),
MuzzleHeight => (ZeroSightGeometry, cont("m", 1e-3, 1e-5), true),
MuzzleAngle => (ZeroSightGeometry, cont("rad", 1e-3, 1e-4), false),
Altitude => (Atmosphere, cont("m", 1e-3, 1.0), false),
Temperature => (Atmosphere, cont("K", 1e-3, 0.05), false),
Pressure => (Atmosphere, cont("Pa", 1e-3, 10.0), false),
RelativeHumidity => (Atmosphere, cont("", 1e-3, 1e-3), false),
Latitude => (Atmosphere, cont("rad", 1e-3, 1e-5), false),
WindSpeed => (Wind, cont("m/s", 1e-3, 0.05), false),
WindDirection => (Wind, cont("rad", 1e-3, 1e-4), false),
WindVertical => (Wind, cont("m/s", 1e-3, 0.05), false),
TargetDistance => (ShotGeometry, cont("m", 1e-3, 0.5), false),
ShootingAngle => (ShotGeometry, cont("rad", 1e-3, 1e-4), false),
Cant => (ShotGeometry, cont("rad", 1e-3, 1e-4), false),
ShotAzimuth => (ShotGeometry, cont("rad", 1e-3, 1e-4), false),
AimAzimuth => (ShotGeometry, cont("rad", 1e-3, 1e-4), false),
TargetHeight => (ShotGeometry, cont("m", 1e-3, 1e-3), false),
MagnusEnabled => (Effects, AxisKind::Categorical, false),
CoriolisEnabled => (Effects, AxisKind::Categorical, false),
EnhancedSpinDriftEnabled => (Effects, AxisKind::Categorical, false),
};
AxisMeta {
group,
kind,
requires_rezero,
}
}
pub fn axes_in_group(group: InputGroup) -> &'static [InputAxis] {
use InputAxis::*;
match group {
InputGroup::ProjectileDrag => &[
Mass,
Diameter,
Length,
BallisticCoefficient,
TwistRate,
TwistDirection,
DragModel,
],
InputGroup::MuzzleVelocity => &[MuzzleVelocityMps],
InputGroup::ZeroSightGeometry => &[
MuzzleAngle,
SightHeight,
ZeroDistance,
ZeroPoiUp,
ZeroPoiRight,
SightOffsetLateral,
MuzzleHeight,
],
InputGroup::Atmosphere => &[Altitude, Temperature, Pressure, RelativeHumidity, Latitude],
InputGroup::Wind => &[WindSpeed, WindDirection, WindVertical],
InputGroup::ShotGeometry => &[
TargetDistance,
ShootingAngle,
Cant,
ShotAzimuth,
AimAzimuth,
TargetHeight,
],
InputGroup::Effects => &[MagnusEnabled, CoriolisEnabled, EnhancedSpinDriftEnabled],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn taxonomy_is_a_partition() {
let mut seen = Vec::new();
for g in InputGroup::ALL {
for a in axes_in_group(*g) {
assert_eq!(
axis_meta(*a).group,
*g,
"{a:?} listed under the wrong group"
);
assert!(!seen.contains(a), "{a:?} appears in more than one group");
seen.push(*a);
}
}
assert_eq!(
seen.len(),
InputAxis::ALL.len(),
"axes_in_group() covers {} axes; InputAxis::ALL has {}",
seen.len(),
InputAxis::ALL.len()
);
for axis in InputAxis::ALL {
assert!(
seen.contains(axis),
"{axis:?} is in InputAxis::ALL but not in any group"
);
}
for axis in InputAxis::ALL {
let _ = axis_meta(*axis);
}
}
#[test]
fn effects_are_categorical() {
for a in axes_in_group(InputGroup::Effects) {
assert!(
matches!(axis_meta(*a).kind, AxisKind::Categorical),
"{a:?} must be Categorical"
);
}
}
#[test]
fn only_zero_affecting_axes_require_rezero() {
assert!(axis_meta(InputAxis::SightHeight).requires_rezero);
assert!(axis_meta(InputAxis::ZeroDistance).requires_rezero);
assert!(!axis_meta(InputAxis::WindSpeed).requires_rezero);
assert!(!axis_meta(InputAxis::TargetDistance).requires_rezero);
}
}