// flight_model.rs
use crate::aircraft::AircraftConfig;
use crate::consts::PHYSICS_DT;
#[derive(Clone)]
pub struct FlightModel {
// Airplane position in flight model coordinate system
// Note: airplane_x = North direction, airplane_y = East direction
// This differs from standard ENU where X=East, Y=North
pub airplane_x: f64, // North position (feet)
pub airplane_y: f64, // East position (feet)
pub airplane_z: f64, // Altitude (feet)
// Angles
pub side_tilt: f64, // sideTiltRadians in banks.c
pub forward_tilt: f64, // forwardTiltRadians in banks.c
pub compass: f64, // compassRadians in banks.c
// Flight parameters
pub speed: f64, // target speed (throttle setting)
pub speed_feet: f64, // current speed in feet/sec // User inputs
pub left_right: f64, // for turning
pub up_down: f64, // for pitch
// Autopilot system
pub autopilot_enabled: bool,
pub target_altitude: f64,
pub target_heading: f64,
// new state for the PD controller
pub autopilot_prev_hdg_error: f64,
pub autopilot_prev_alt_error: f64,
pub autopilot_filtered_hdg_deriv: f64, // Filtered derivative term to reduce noise
// Waypoint navigation
pub waypoint_enabled: bool,
pub waypoints: Vec,
pub current_waypoint_index: usize,
pub waypoint_target_heading: f64, // The actual bearing to waypoint (for HUD display) // HUD display options
pub show_coordinates: bool, // Toggle coordinate display with X key
pub show_scene_labels: bool, // Toggle scene origin labels with M key
// Global coordinate system (set by event loop during recentering)
pub current_lat: f64,
pub current_lon: f64,
// Reference coordinates for ENU conversion (set by event loop)
pub ref_lat: f64,
pub ref_lon: f64,
engine_count: u32,
power_hp: f64,
mass_slugs: f64,
cd0: f64,
cl_max: f64,
aspect_ratio: f64,
critical_aoa_rad: f64,
stall_pitch_down_rad: f64,
stall_speed_fps: f64,
thrust_lbf: f64,
// Rotation matrix (camera transform)
pub r11: f64, pub r12: f64, pub r13: f64,
pub r21: f64, pub r22: f64, pub r23: f64,
pub r31: f64, pub r32: f64, pub r33: f64,
// Additional physics variables from banks.c
d: f64,
f: f64,
p: f64,
t: f64,
v: f64,
w: f64,
m: f64,
e: f64,
i: f64,
h: f64,
x: f64,
gravity_accel: f64,
s: f64,
}
impl FlightModel {
pub fn new(config: AircraftConfig) -> Self {
let mut model = Self {
airplane_x: 0.0,
airplane_y: 0.0,
airplane_z: config.initial_altitude_ft,
side_tilt: 0.0,
forward_tilt: config.initial_forward_tilt_rad,
compass: 0.0,
speed: 8.0, // Still fixed throttle steps for now
speed_feet: config.initial_speed_fps,
left_right: 0.0,
up_down: 0.0, // Initialize autopilot system
autopilot_enabled: false,
target_altitude: 0.0,
target_heading: 0.0,
autopilot_prev_hdg_error: 0.0,
autopilot_prev_alt_error: 0.0,
autopilot_filtered_hdg_deriv: 0.0,
// Initialize waypoint navigation
waypoint_enabled: false,
waypoints: Vec::new(),
current_waypoint_index: 0,
waypoint_target_heading: 0.0,
// HUD display options
show_coordinates: true, // Enabled by default, toggle with X key
show_scene_labels: true, // Enabled by default, toggle with M key
// Global coordinate system (set by event loop during recentering)
current_lat: 37.6213, // Default SFO coordinates
current_lon: -122.3790,
// Reference coordinates for ENU conversion (set by event loop)
ref_lat: 37.6213, // Default SFO coordinates
ref_lon: -122.3790,
r11: 1.0, r12: 0.0, r13: 0.0,
r21: 0.0, r22: 1.0, r23: 0.0,
r31: 0.0, r32: 0.0, r33: 1.0,
d: 0.0,
f: 0.0,
p: 0.0,
t: 0.0,
v: 0.0,
w: 0.0,
m: 0.0,
e: 0.0,
i: 0.0,
h: 0.0,
x: config.initial_lift_param,
gravity_accel: config.gravity_accel,
s: config.wing_area_ft2,
mass_slugs: config.mass_lb / 32.174,
engine_count: config.engine_count,
// Aircraft config
power_hp: config.power_hp,
cd0: config.cd0,
cl_max: config.cl_max,
aspect_ratio: config.aspect_ratio,
critical_aoa_rad: config.critical_aoa_rad,
stall_pitch_down_rad: config.stall_pitch_down_rad,
stall_speed_fps: config.stall_speed_kts * 1.68781,
thrust_lbf: config.thrust_lbf,
};
model.calculate_angles();
model
}
// Throttle control - increases target speed (directly impacts acceleration)
pub fn increase_throttle(&mut self) {
self.speed += 1.0;
}
// Throttle control - decreases target speed
pub fn decrease_throttle(&mut self) {
self.speed -= 1.0;
if self.speed < 0.0 {
self.speed = 0.0; // Prevent negative speed
}
}
/// Calculate the new orientation angles and rotation matrix
pub fn calculate_angles(&mut self) {
// Trigonometric precomputations
let cos_fwd = self.forward_tilt.cos();
let sin_fwd = self.forward_tilt.sin();
let cos_cmp = self.compass.cos();
let cos_side = self.side_tilt.cos();
let sin_side = self.side_tilt.sin();
let sin_cmp = self.compass.sin();
// Update F (moment arm)
self.f += PHYSICS_DT * self.p;
// Yaw (compass heading)
self.compass += cos_side * PHYSICS_DT * self.f / cos_fwd
+ self.d / cos_fwd * sin_side * PHYSICS_DT;
// Pitch (forward/back tilt)
self.forward_tilt += self.d * PHYSICS_DT * cos_side
- PHYSICS_DT * self.f * sin_side;
// Optional: crude stall behavior (nose drop)
if self.speed_feet < self.stall_speed_fps && self.forward_tilt > self.critical_aoa_rad {
self.forward_tilt = self.stall_pitch_down_rad;
}
// Roll (sideways tilt)
self.side_tilt += (sin_side * self.d / cos_fwd * sin_fwd
+ self.v
+ sin_fwd / cos_fwd * self.f * cos_side) * PHYSICS_DT;
// Build rotation matrix
self.r11 = cos_fwd * cos_cmp;
self.r12 = cos_fwd * sin_cmp;
self.r13 = -sin_fwd;
self.r21 = cos_cmp * sin_side * sin_fwd - sin_cmp * cos_side;
self.r22 = cos_side * cos_cmp + sin_side * sin_cmp * sin_fwd;
self.r23 = sin_side * cos_fwd;
self.r31 = sin_cmp * sin_side + cos_side * sin_fwd * cos_cmp;
self.r32 = sin_fwd * sin_cmp * cos_side - sin_side * cos_cmp;
self.r33 = cos_side * cos_fwd;
}
/// Update physics: momentum, position, forces, and rates
pub fn update_physics(&mut self) {
// 1) Momentum
let local_momentum = self.h * PHYSICS_DT;
self.m += local_momentum;
// 2) Inertia
self.i = self.m / self.speed_feet;
// 3) Position update (world → airplane reference)
self.airplane_x += (self.r11 * self.speed_feet + self.r21 * self.m + self.r31 * self.x) * PHYSICS_DT;
self.airplane_y += (self.r12 * self.speed_feet + self.i * self.m + self.r32 * self.x) * PHYSICS_DT;
self.airplane_z += (-self.r13 * self.speed_feet - self.r23 * self.m - self.r33 * self.x) * PHYSICS_DT;
// 4) Intermediate values
let local_intermediate = 15.0 * self.f / self.speed_feet;
self.e = 0.1 + self.x * 4.9 / self.speed_feet;
self.t = self.x * self.x
+ self.speed_feet * self.speed_feet
+ self.m * self.m; // now using momentum squared
let t_temp = self.t * local_intermediate / 32.0
- self.i * self.t / 24.0;
// 5) H (for next momentum)
self.h = self.gravity_accel * self.r23
+ self.v * self.x
- self.f * self.speed_feet
+ t_temp / self.s;
// 6) Acceleration along fuselage
// Atmospheric constants
let rho = 0.0023769; // slugs/ft³ at sea level
let q = 0.5 * rho * self.speed_feet.powi(2); // dynamic pressure
// Approximate angle of attack from pitch angle (forward_tilt)
let aoa = self.forward_tilt;
// Clamp CL to critical AoA
let cl = if aoa.abs() > self.critical_aoa_rad {
self.cl_max * (1.0 - ((aoa.abs() - self.critical_aoa_rad) / self.critical_aoa_rad).min(1.0))
} else {
self.cl_max * aoa / self.critical_aoa_rad
};
// Drag = q * S * (cd0 + cl² / (π * AR))
let induced_drag = cl.powi(2) / (std::f64::consts::PI * self.aspect_ratio);
// Reduce drag coefficient to make it more realistic - small planes have less drag
let drag_coefficient = self.cd0 + induced_drag * 0.5; // Apply 50% factor to induced drag
let drag_force = q * self.s * drag_coefficient;
// Use the configured thrust directly instead of calculating from power
// Scale with throttle (range 0-10)
let throttle_ratio = (self.speed / 10.0).min(1.0).max(0.0);
// Use direct thrust with throttle control
let thrust_force = self.thrust_lbf * throttle_ratio * self.engine_count as f64;
// We can also calculate additional thrust from power for more realistic engine modeling
// Power = Force * Velocity, so Force = Power / Velocity
let power_thrust = if self.speed_feet > 20.0 { // Only apply at reasonable speeds
self.power_hp * 550.0 * throttle_ratio / self.speed_feet
} else {
0.0 // Avoid division by small numbers
};
// Use the greater of the two thrust values - at low speed use configured thrust,
// at higher speeds might transition to power-based thrust
let thrust_force = thrust_force.max(power_thrust);
// Gravity force in body X (forward) direction
let gravity_force = -self.mass_slugs * self.gravity_accel * self.forward_tilt.sin();
// Net force = thrust - drag + gravity component
let net_force = thrust_force - drag_force + gravity_force;
// F = ma ⇒ a = F / m
let accel = net_force / self.mass_slugs;
// 7) Speed update
self.speed_feet += accel * PHYSICS_DT;
if self.speed_feet < 0.1 {
self.speed_feet = 0.1;
}
// 8) Next‐step states
let a = 2.63 / self.speed_feet * self.d;
let m_momentum = self.m; // ← accumulate momentum from step 1
self.x += (self.d * self.speed_feet
- self.t / self.s * (0.19 * self.e + a * 0.64 + self.up_down / 1e3)
- m_momentum * self.v // now matches banks.c’s M * v
+ self.gravity_accel * self.r33) * PHYSICS_DT;
self.w = self.d;
self.d += self.t * (0.45 - 14.0 / self.speed_feet * self.x - a * 130.0 - self.up_down * 0.14)
* PHYSICS_DT / 125e2
+ self.f * PHYSICS_DT * self.v;
let d_param = self.v / self.speed_feet * 15.0;
// 9) Turn rate P
self.p = (self.t * (47.0 * self.i
- local_intermediate * 52.0
+ self.e * 94.0 * d_param
- t_temp * 0.38
+ self.left_right * 0.21 * self.e) / 1e2
+ self.w * 179.0 * self.v) / 2312.0;
// 10) Sideslip v
self.v -= (self.w * self.f
- self.t * (0.63 * local_intermediate
- self.i * 0.086
+ local_intermediate * self.e * 19.0
- d_param * 25.0
- 0.11 * self.left_right) / 107e2) * PHYSICS_DT;
}
}