1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum InputGroup {
12 ProjectileDrag, MuzzleVelocity, ZeroSightGeometry,
13 Atmosphere, Wind, ShotGeometry, Effects,
14}
15
16impl InputGroup {
17 pub const ALL: &'static [InputGroup] = &[
18 InputGroup::ProjectileDrag, InputGroup::MuzzleVelocity, InputGroup::ZeroSightGeometry,
19 InputGroup::Atmosphere, InputGroup::Wind, InputGroup::ShotGeometry, InputGroup::Effects,
20 ];
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum InputAxis {
26 Mass, Diameter, Length, BallisticCoefficient, TwistRate, TwistDirection, DragModel,
27 MuzzleVelocityMps,
28 SightHeight, ZeroDistance, ZeroPoiUp, ZeroPoiRight, SightOffsetLateral, MuzzleHeight, MuzzleAngle,
29 Altitude, Temperature, Pressure, RelativeHumidity, Latitude,
30 WindSpeed, WindDirection, WindVertical,
31 TargetDistance, ShootingAngle, Cant, ShotAzimuth, AimAzimuth, TargetHeight,
32 MagnusEnabled, CoriolisEnabled, EnhancedSpinDriftEnabled,
33}
34
35impl InputAxis {
36 pub const ALL: &'static [InputAxis] = &[
37 InputAxis::Mass, InputAxis::Diameter, InputAxis::Length, InputAxis::BallisticCoefficient,
38 InputAxis::TwistRate, InputAxis::TwistDirection, InputAxis::DragModel,
39 InputAxis::MuzzleVelocityMps,
40 InputAxis::SightHeight, InputAxis::ZeroDistance, InputAxis::ZeroPoiUp, InputAxis::ZeroPoiRight,
41 InputAxis::SightOffsetLateral, InputAxis::MuzzleHeight, InputAxis::MuzzleAngle,
42 InputAxis::Altitude, InputAxis::Temperature, InputAxis::Pressure, InputAxis::RelativeHumidity,
43 InputAxis::Latitude,
44 InputAxis::WindSpeed, InputAxis::WindDirection, InputAxis::WindVertical,
45 InputAxis::TargetDistance, InputAxis::ShootingAngle, InputAxis::Cant, InputAxis::ShotAzimuth,
46 InputAxis::AimAzimuth, InputAxis::TargetHeight,
47 InputAxis::MagnusEnabled, InputAxis::CoriolisEnabled, InputAxis::EnhancedSpinDriftEnabled,
48 ];
49}
50
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub enum AxisKind {
53 Continuous { unit: &'static str, default_rel_step: f64, min_abs_step: f64 },
56 Categorical,
58}
59
60#[derive(Debug, Clone, Copy)]
61pub struct AxisMeta {
62 pub group: InputGroup,
63 pub kind: AxisKind,
64 pub requires_rezero: bool,
67}
68
69const fn cont(unit: &'static str, rel: f64, min_abs: f64) -> AxisKind {
70 AxisKind::Continuous { unit, default_rel_step: rel, min_abs_step: min_abs }
71}
72
73pub fn axis_meta(axis: InputAxis) -> AxisMeta {
74 use InputAxis::*;
75 use InputGroup::*;
76 let (group, kind, requires_rezero) = match axis {
77 Mass => (ProjectileDrag, cont("kg", 1e-3, 1e-7), true),
78 Diameter => (ProjectileDrag, cont("m", 1e-3, 1e-7), true),
79 Length => (ProjectileDrag, cont("m", 1e-3, 1e-6), false),
80 BallisticCoefficient => (ProjectileDrag, cont("", 1e-3, 1e-4), true),
81 TwistRate => (ProjectileDrag, cont("m/turn", 1e-3, 1e-5), false),
82 TwistDirection => (ProjectileDrag, AxisKind::Categorical, false),
83 DragModel => (ProjectileDrag, AxisKind::Categorical, true),
84 MuzzleVelocityMps => (MuzzleVelocity, cont("m/s", 1e-3, 0.15), true),
85 SightHeight => (ZeroSightGeometry, cont("m", 1e-3, 1e-5), true),
86 ZeroDistance => (ZeroSightGeometry, cont("m", 1e-3, 0.1), true),
87 ZeroPoiUp => (ZeroSightGeometry, cont("m", 1e-3, 1e-5), true),
88 ZeroPoiRight => (ZeroSightGeometry, cont("m", 1e-3, 1e-5), true),
89 SightOffsetLateral => (ZeroSightGeometry, cont("m", 1e-3, 1e-5), true),
90 MuzzleHeight => (ZeroSightGeometry, cont("m", 1e-3, 1e-5), true),
91 MuzzleAngle => (ZeroSightGeometry, cont("rad", 1e-3, 1e-4), false),
92 Altitude => (Atmosphere, cont("m", 1e-3, 1.0), false),
93 Temperature => (Atmosphere, cont("K", 1e-3, 0.05), false),
94 Pressure => (Atmosphere, cont("Pa", 1e-3, 10.0), false),
95 RelativeHumidity => (Atmosphere, cont("", 1e-3, 1e-3), false),
96 Latitude => (Atmosphere, cont("rad", 1e-3, 1e-5), false),
97 WindSpeed => (Wind, cont("m/s", 1e-3, 0.05), false),
98 WindDirection => (Wind, cont("rad", 1e-3, 1e-4), false),
99 WindVertical => (Wind, cont("m/s", 1e-3, 0.05), false),
100 TargetDistance => (ShotGeometry, cont("m", 1e-3, 0.5), false),
101 ShootingAngle => (ShotGeometry, cont("rad", 1e-3, 1e-4), false),
102 Cant => (ShotGeometry, cont("rad", 1e-3, 1e-4), false),
103 ShotAzimuth => (ShotGeometry, cont("rad", 1e-3, 1e-4), false),
104 AimAzimuth => (ShotGeometry, cont("rad", 1e-3, 1e-4), false),
105 TargetHeight => (ShotGeometry, cont("m", 1e-3, 1e-3), false),
106 MagnusEnabled => (Effects, AxisKind::Categorical, false),
107 CoriolisEnabled => (Effects, AxisKind::Categorical, false),
108 EnhancedSpinDriftEnabled => (Effects, AxisKind::Categorical, false),
109 };
110 AxisMeta { group, kind, requires_rezero }
111}
112
113pub fn axes_in_group(group: InputGroup) -> &'static [InputAxis] {
114 use InputAxis::*;
115 match group {
116 InputGroup::ProjectileDrag => &[Mass, Diameter, Length, BallisticCoefficient, TwistRate, TwistDirection, DragModel],
117 InputGroup::MuzzleVelocity => &[MuzzleVelocityMps],
118 InputGroup::ZeroSightGeometry =>
132 &[MuzzleAngle, SightHeight, ZeroDistance, ZeroPoiUp, ZeroPoiRight, SightOffsetLateral, MuzzleHeight],
133 InputGroup::Atmosphere => &[Altitude, Temperature, Pressure, RelativeHumidity, Latitude],
134 InputGroup::Wind => &[WindSpeed, WindDirection, WindVertical],
135 InputGroup::ShotGeometry =>
136 &[TargetDistance, ShootingAngle, Cant, ShotAzimuth, AimAzimuth, TargetHeight],
137 InputGroup::Effects =>
138 &[MagnusEnabled, CoriolisEnabled, EnhancedSpinDriftEnabled],
139 }
140}
141
142#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
169 fn taxonomy_is_a_partition() {
170 let mut seen = Vec::new();
171 for g in InputGroup::ALL {
172 for a in axes_in_group(*g) {
173 assert_eq!(axis_meta(*a).group, *g, "{a:?} listed under the wrong group");
174 assert!(!seen.contains(a), "{a:?} appears in more than one group");
175 seen.push(*a);
176 }
177 }
178 assert_eq!(seen.len(), InputAxis::ALL.len(),
180 "axes_in_group() covers {} axes; InputAxis::ALL has {}",
181 seen.len(), InputAxis::ALL.len());
182 for axis in InputAxis::ALL {
183 assert!(seen.contains(axis), "{axis:?} is in InputAxis::ALL but not in any group");
184 }
185 for axis in InputAxis::ALL {
187 let _ = axis_meta(*axis);
188 }
189 }
190
191 #[test]
193 fn effects_are_categorical() {
194 for a in axes_in_group(InputGroup::Effects) {
195 assert!(matches!(axis_meta(*a).kind, AxisKind::Categorical),
196 "{a:?} must be Categorical");
197 }
198 }
199
200 #[test]
202 fn only_zero_affecting_axes_require_rezero() {
203 assert!(axis_meta(InputAxis::SightHeight).requires_rezero);
204 assert!(axis_meta(InputAxis::ZeroDistance).requires_rezero);
205 assert!(!axis_meta(InputAxis::WindSpeed).requires_rezero);
206 assert!(!axis_meta(InputAxis::TargetDistance).requires_rezero);
207 }
208}