use avian3d::math::{Quaternion, Scalar, Vector};
use super::coefficients::ZoneCoefficients;
pub(crate) struct ZoneWorldForce {
pub force: Vector,
pub torque: Vector,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn zone_force_world(
coeffs: &ZoneCoefficients,
qbar: Scalar,
zone_area: Scalar,
span_ref: Scalar,
zone_chord: Scalar,
alpha: Scalar,
vel_zone_unit: Vector,
zone_to_world: Quaternion,
) -> ZoneWorldForce {
let drag_zone = vel_zone_unit * (-coeffs.cd * qbar * zone_area);
let stab_to_zone = Quaternion::from_rotation_y(-alpha);
let lift_side_stab = Vector::new(
0.0,
coeffs.cy * qbar * zone_area,
-coeffs.cl * qbar * zone_area,
);
let lift_side_zone = stab_to_zone * lift_side_stab;
let force = zone_to_world * (drag_zone + lift_side_zone);
let torque_zone = Vector::new(
coeffs.croll * qbar * zone_area * span_ref,
coeffs.cm * qbar * zone_area * zone_chord,
coeffs.cn * qbar * zone_area * span_ref,
);
let torque = zone_to_world * torque_zone;
ZoneWorldForce { force, torque }
}
#[cfg(test)]
mod tests {
use super::super::coefficients::ZoneCoefficients;
use super::*;
fn unit_coeffs(cl: Scalar, cd: Scalar, cy: Scalar, cm: Scalar) -> ZoneCoefficients {
ZoneCoefficients {
cl,
cd,
cy,
cm,
croll: 0.0,
cn: 0.0,
}
}
#[test]
fn lift_opposes_gravity_at_zero_alpha() {
let coeffs = unit_coeffs(1.0, 0.0, 0.0, 0.0);
let wf = zone_force_world(
&coeffs,
1000.0,
16.0,
10.0,
1.6,
0.0,
Vector::X,
Quaternion::from_rotation_x(avian3d::math::FRAC_PI_2),
);
assert!(
wf.force.y > 0.0,
"lift should point up (+Y world), got {}",
wf.force.y
);
}
#[test]
fn drag_opposes_forward_motion() {
let coeffs = unit_coeffs(0.0, 1.0, 0.0, 0.0);
let wf = zone_force_world(
&coeffs,
1000.0,
16.0,
10.0,
1.6,
0.0,
Vector::X,
Quaternion::from_rotation_x(avian3d::math::FRAC_PI_2),
);
assert!(
wf.force.x < 0.0,
"drag should oppose forward motion, got {}",
wf.force.x
);
}
#[test]
fn drag_opposes_velocity_not_nose_at_sideslip() {
let coeffs = unit_coeffs(0.0, 1.0, 0.0, 0.0);
let frac_pi_4 = avian3d::math::FRAC_PI_2 / 2.0;
let beta: Scalar = -frac_pi_4;
let vel_body_unit = Vector::new(beta.cos(), beta.sin(), 0.0);
let body_to_world = Quaternion::from_rotation_y(-frac_pi_4)
* Quaternion::from_rotation_x(avian3d::math::FRAC_PI_2);
let wf = zone_force_world(
&coeffs,
1000.0,
16.0,
10.0,
1.6,
0.0,
vel_body_unit,
body_to_world,
);
assert!(
wf.force.x < 0.0,
"drag x should be negative, got {}",
wf.force.x
);
assert!(
wf.force.z.abs() < wf.force.x.abs() * 0.01,
"drag should not push sideways, z={}",
wf.force.z
);
}
#[test]
fn side_force_rotates_with_aircraft() {
let cy = 1.0;
let coeffs = unit_coeffs(0.0, 0.0, cy, 0.0);
let frac_pi_2 = avian3d::math::FRAC_PI_2;
let body_to_world_level = Quaternion::from_rotation_x(frac_pi_2);
let wf_level = zone_force_world(
&coeffs,
1000.0,
16.0,
10.0,
1.6,
0.0,
Vector::X,
body_to_world_level,
);
let body_to_world_yawed =
Quaternion::from_rotation_y(-frac_pi_2) * Quaternion::from_rotation_x(frac_pi_2);
let beta_90: Scalar = -frac_pi_2;
let vel_body_unit_yawed = Vector::new(beta_90.cos(), beta_90.sin(), 0.0);
let wf_yawed = zone_force_world(
&coeffs,
1000.0,
16.0,
10.0,
1.6,
0.0,
vel_body_unit_yawed,
body_to_world_yawed,
);
let dir_level = wf_level.force.normalize();
let dir_yawed = wf_yawed.force.normalize();
let dot = dir_level.dot(dir_yawed);
assert!(
dot.abs() < 0.1,
"CY direction should differ by ~90 deg after 90 deg yaw, dot={dot}"
);
}
#[test]
fn force_scales_with_dynamic_pressure() {
let coeffs = unit_coeffs(0.5, 0.0, 0.0, 0.0);
let wf1 = zone_force_world(
&coeffs,
500.0,
16.0,
10.0,
1.6,
0.0,
Vector::X,
Quaternion::IDENTITY,
);
let wf2 = zone_force_world(
&coeffs,
2000.0,
16.0,
10.0,
1.6,
0.0,
Vector::X,
Quaternion::IDENTITY,
);
let ratio = wf2.force.length() / wf1.force.length();
assert!(
(ratio - 4.0).abs() < 1e-4,
"force should scale 4:1 with q̄, ratio = {ratio}"
);
}
#[test]
fn pitching_moment_uses_chord_not_span() {
let coeffs = unit_coeffs(0.0, 0.0, 0.0, 1.0);
let wf_a = zone_force_world(
&coeffs,
1000.0,
1.0,
10.0,
2.0,
0.0,
Vector::X,
Quaternion::IDENTITY,
);
let wf_b = zone_force_world(
&coeffs,
1000.0,
1.0,
10.0,
4.0,
0.0,
Vector::X,
Quaternion::IDENTITY,
);
let ratio = wf_b.torque.y / wf_a.torque.y;
assert!(
(ratio - 2.0).abs() < 1e-4,
"CM should scale with chord, ratio = {ratio}"
);
}
}