use glam::{DQuat, DVec3};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SurfaceShape {
FlatPlate {
area: f64,
normal: DVec3,
},
Cylinder {
radius: f64,
length: f64,
axis: DVec3,
},
ConicalFrustum {
radius_bottom: f64,
radius_top: f64,
length: f64,
axis: DVec3,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SurfaceFacet {
pub shape: SurfaceShape,
pub center: DVec3,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ArticulationState {
pub axis: DVec3,
pub angle: f64,
pub rate: f64,
}
impl ArticulationState {
#[inline]
pub fn rotation(&self) -> DQuat {
let len2 = self.axis.length_squared();
if len2 <= 0.0 {
DQuat::IDENTITY
} else {
DQuat::from_axis_angle(self.axis / len2.sqrt(), self.angle)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ArticulatedFacet {
pub facet: SurfaceFacet,
pub articulation: Option<ArticulationState>,
}
impl ArticulatedFacet {
#[inline]
pub fn fixed(facet: SurfaceFacet) -> Self {
Self {
facet,
articulation: None,
}
}
#[inline]
pub fn articulated(facet: SurfaceFacet, articulation: ArticulationState) -> Self {
Self {
facet,
articulation: Some(articulation),
}
}
pub fn current_shape(&self) -> SurfaceShape {
match self.articulation {
None => self.facet.shape,
Some(art) => {
let q = art.rotation();
match self.facet.shape {
SurfaceShape::FlatPlate { area, normal } => SurfaceShape::FlatPlate {
area,
normal: q * normal,
},
SurfaceShape::Cylinder {
radius,
length,
axis,
} => SurfaceShape::Cylinder {
radius,
length,
axis: q * axis,
},
SurfaceShape::ConicalFrustum {
radius_bottom,
radius_top,
length,
axis,
} => SurfaceShape::ConicalFrustum {
radius_bottom,
radius_top,
length,
axis: q * axis,
},
}
}
}
}
pub fn advance(&mut self, dt: f64) {
if let Some(ref mut art) = self.articulation {
art.angle += art.rate * dt;
}
}
}
impl SurfaceShape {
pub fn projected_area(&self, flow_direction: DVec3) -> f64 {
let flow_len2 = flow_direction.length_squared();
if flow_len2 <= 0.0 {
return 0.0;
}
let v_hat = flow_direction / flow_len2.sqrt();
match *self {
SurfaceShape::FlatPlate { area, normal } => {
let n = normalize_or_zero(normal);
area * n.dot(v_hat).abs()
}
SurfaceShape::Cylinder {
radius,
length,
axis,
} => {
let a = normalize_or_zero(axis);
if a == DVec3::ZERO {
return 0.0;
}
let cos_theta = a.dot(v_hat);
let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt();
2.0 * radius * length * sin_theta
+ std::f64::consts::PI * radius * radius * cos_theta.abs()
}
SurfaceShape::ConicalFrustum {
radius_bottom,
radius_top,
length,
axis,
} => {
let a = normalize_or_zero(axis);
if a == DVec3::ZERO {
return 0.0;
}
let cos_theta = a.dot(v_hat);
let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt();
let r_forward = if cos_theta > 0.0 {
radius_bottom
} else {
radius_top
};
(radius_bottom + radius_top) * length * sin_theta
+ std::f64::consts::PI * r_forward * r_forward * cos_theta.abs()
}
}
}
pub fn effective_normal(&self, flow_direction: DVec3) -> DVec3 {
let flow_len2 = flow_direction.length_squared();
if flow_len2 <= 0.0 {
return DVec3::ZERO;
}
let v_hat = flow_direction / flow_len2.sqrt();
match *self {
SurfaceShape::FlatPlate { normal, .. } => {
let n = normalize_or_zero(normal);
if n.dot(v_hat) > 0.0 {
-n
} else {
n
}
}
SurfaceShape::Cylinder { axis, .. } | SurfaceShape::ConicalFrustum { axis, .. } => {
if normalize_or_zero(axis) == DVec3::ZERO {
DVec3::ZERO
} else {
-v_hat
}
}
}
}
}
#[inline]
fn normalize_or_zero(v: DVec3) -> DVec3 {
let len2 = v.length_squared();
if len2 <= 0.0 {
DVec3::ZERO
} else {
v / len2.sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::{FRAC_PI_4, PI};
const TOL: f64 = 1e-12;
#[test]
fn flat_plate_projected_area_normal_flow() {
let shape = SurfaceShape::FlatPlate {
area: 10.0,
normal: DVec3::X,
};
let projected = shape.projected_area(DVec3::X);
assert!(
(projected - 10.0).abs() < TOL,
"Expected 10.0, got {projected}"
);
}
#[test]
fn flat_plate_projected_area_parallel_flow() {
let shape = SurfaceShape::FlatPlate {
area: 10.0,
normal: DVec3::X,
};
let projected = shape.projected_area(DVec3::Y);
assert!(projected.abs() < TOL, "Expected 0, got {projected}");
}
#[test]
fn flat_plate_projected_area_symmetric() {
let shape = SurfaceShape::FlatPlate {
area: 7.5,
normal: DVec3::new(1.0, 1.0, 0.0).normalize(),
};
let v = DVec3::X;
let a_forward = shape.projected_area(v);
let a_backward = shape.projected_area(-v);
assert!((a_forward - a_backward).abs() < TOL);
}
#[test]
fn flat_plate_projected_area_45_degrees() {
let shape = SurfaceShape::FlatPlate {
area: 10.0,
normal: DVec3::new(1.0, 1.0, 0.0).normalize(),
};
let projected = shape.projected_area(DVec3::X);
let expected = 10.0 * (1.0 / 2.0_f64.sqrt());
assert!(
(projected - expected).abs() < TOL,
"Expected {expected}, got {projected}"
);
}
#[test]
fn cylinder_projected_area_axial_flow() {
let r = 2.0;
let l = 5.0;
let shape = SurfaceShape::Cylinder {
radius: r,
length: l,
axis: DVec3::X,
};
let projected = shape.projected_area(DVec3::X);
let expected = PI * r * r;
assert!(
(projected - expected).abs() < TOL,
"Axial flow: expected {expected}, got {projected}"
);
}
#[test]
fn cylinder_projected_area_transverse_flow() {
let r = 2.0;
let l = 5.0;
let shape = SurfaceShape::Cylinder {
radius: r,
length: l,
axis: DVec3::Y,
};
let projected = shape.projected_area(DVec3::X);
let expected = 2.0 * r * l;
assert!(
(projected - expected).abs() < TOL,
"Transverse flow: expected {expected}, got {projected}"
);
}
#[test]
fn cylinder_projected_area_oblique() {
let r = 2.0;
let l = 5.0;
let axis = DVec3::new(1.0, 1.0, 0.0).normalize();
let shape = SurfaceShape::Cylinder {
radius: r,
length: l,
axis,
};
let projected = shape.projected_area(DVec3::X);
let c = FRAC_PI_4.cos();
let s = FRAC_PI_4.sin();
let expected = PI * r * r * c + 2.0 * r * l * s;
assert!(
(projected - expected).abs() < TOL,
"45° cylinder: expected {expected}, got {projected}"
);
}
#[test]
fn cylinder_projected_area_axis_symmetry() {
let shape_plus = SurfaceShape::Cylinder {
radius: 1.5,
length: 4.0,
axis: DVec3::X,
};
let shape_minus = SurfaceShape::Cylinder {
radius: 1.5,
length: 4.0,
axis: -DVec3::X,
};
let v = DVec3::new(1.0, 0.5, 0.0).normalize();
assert!(
(shape_plus.projected_area(v) - shape_minus.projected_area(v)).abs() < TOL,
"Cylinder should be symmetric under axis flip"
);
}
#[test]
fn cone_projected_area_degenerates_to_cylinder() {
let r = 2.5;
let l = 6.0;
let axis = DVec3::Z;
let cyl = SurfaceShape::Cylinder {
radius: r,
length: l,
axis,
};
let frust = SurfaceShape::ConicalFrustum {
radius_bottom: r,
radius_top: r,
length: l,
axis,
};
for v in [DVec3::X, DVec3::Y, DVec3::Z, DVec3::new(1.0, 1.0, 1.0)] {
let cyl_a = cyl.projected_area(v);
let fr_a = frust.projected_area(v);
assert!(
(cyl_a - fr_a).abs() < TOL,
"Degenerate frustum ≠ cylinder for v={v:?}: {cyl_a} vs {fr_a}"
);
}
}
#[test]
fn cone_projected_area_axial_bottom_forward() {
let rb = 3.0;
let rt = 1.0;
let l = 4.0;
let shape = SurfaceShape::ConicalFrustum {
radius_bottom: rb,
radius_top: rt,
length: l,
axis: DVec3::X,
};
let projected = shape.projected_area(DVec3::X);
let expected = PI * rb * rb;
assert!(
(projected - expected).abs() < TOL,
"Axial (bottom forward): expected {expected}, got {projected}"
);
}
#[test]
fn cone_projected_area_axial_top_forward() {
let rb = 3.0;
let rt = 1.0;
let l = 4.0;
let shape = SurfaceShape::ConicalFrustum {
radius_bottom: rb,
radius_top: rt,
length: l,
axis: DVec3::X,
};
let projected = shape.projected_area(-DVec3::X);
let expected = PI * rt * rt;
assert!(
(projected - expected).abs() < TOL,
"Axial (top forward): expected {expected}, got {projected}"
);
}
#[test]
fn cone_projected_area_transverse() {
let rb = 3.0;
let rt = 1.0;
let l = 4.0;
let shape = SurfaceShape::ConicalFrustum {
radius_bottom: rb,
radius_top: rt,
length: l,
axis: DVec3::X,
};
let projected = shape.projected_area(DVec3::Y);
let expected = (rb + rt) * l;
assert!(
(projected - expected).abs() < TOL,
"Transverse: expected {expected}, got {projected}"
);
}
#[test]
fn cone_projected_area() {
let rb = 3.0;
let rt = 1.0;
let l = 4.0;
let shape = SurfaceShape::ConicalFrustum {
radius_bottom: rb,
radius_top: rt,
length: l,
axis: DVec3::X,
};
let v = DVec3::new(0.5, 3.0_f64.sqrt() / 2.0, 0.0);
let projected = shape.projected_area(v);
let cos_theta: f64 = 0.5;
let sin_theta: f64 = 3.0_f64.sqrt() / 2.0;
let expected = (rb + rt) * l * sin_theta + PI * rb * rb * cos_theta.abs();
assert!(
(projected - expected).abs() < TOL,
"Oblique frustum: expected {expected}, got {projected}"
);
}
#[test]
fn effective_normal_plate_faces_flow() {
let shape = SurfaceShape::FlatPlate {
area: 1.0,
normal: DVec3::X, };
let eff = shape.effective_normal(DVec3::X);
assert!((eff - (-DVec3::X)).length() < TOL, "got {eff:?}");
}
#[test]
fn effective_normal_plate_back_faces_flow() {
let shape = SurfaceShape::FlatPlate {
area: 1.0,
normal: -DVec3::X,
};
let eff = shape.effective_normal(DVec3::X);
assert!((eff - (-DVec3::X)).length() < TOL);
}
#[test]
fn effective_normal_cylinder_along_flow() {
let shape = SurfaceShape::Cylinder {
radius: 1.0,
length: 2.0,
axis: DVec3::Y,
};
let v = DVec3::new(1.0, 0.0, 0.0);
let eff = shape.effective_normal(v);
assert!((eff - (-DVec3::X)).length() < TOL);
}
#[test]
fn articulation_rotates_normal() {
let base = SurfaceFacet {
shape: SurfaceShape::FlatPlate {
area: 4.0,
normal: DVec3::X,
},
center: DVec3::ZERO,
};
let art = ArticulationState {
axis: DVec3::Z,
angle: std::f64::consts::FRAC_PI_2,
rate: 0.0,
};
let facet = ArticulatedFacet::articulated(base, art);
match facet.current_shape() {
SurfaceShape::FlatPlate { normal, area } => {
assert!((normal - DVec3::Y).length() < 1e-12, "got {normal:?}");
assert_eq!(area, 4.0);
}
_ => panic!("expected FlatPlate"),
}
}
#[test]
fn articulation_rotates_cylinder_axis() {
let base = SurfaceFacet {
shape: SurfaceShape::Cylinder {
radius: 1.0,
length: 2.0,
axis: DVec3::X,
},
center: DVec3::ZERO,
};
let art = ArticulationState {
axis: DVec3::Z,
angle: std::f64::consts::FRAC_PI_2,
rate: 0.0,
};
let facet = ArticulatedFacet::articulated(base, art);
match facet.current_shape() {
SurfaceShape::Cylinder {
axis,
radius,
length,
} => {
assert!((axis - DVec3::Y).length() < 1e-12, "got {axis:?}");
assert_eq!(radius, 1.0);
assert_eq!(length, 2.0);
}
_ => panic!("expected Cylinder"),
}
}
#[test]
fn articulation_advance_integrates_rate() {
let base = SurfaceFacet {
shape: SurfaceShape::FlatPlate {
area: 1.0,
normal: DVec3::X,
},
center: DVec3::ZERO,
};
let art = ArticulationState {
axis: DVec3::Z,
angle: 0.0,
rate: 0.1, };
let mut facet = ArticulatedFacet::articulated(base, art);
facet.advance(5.0);
assert!(
(facet.articulation.unwrap().angle - 0.5).abs() < 1e-15,
"angle = {}",
facet.articulation.unwrap().angle
);
}
#[test]
fn fixed_facet_is_noop() {
let base = SurfaceFacet {
shape: SurfaceShape::FlatPlate {
area: 4.0,
normal: DVec3::X,
},
center: DVec3::ZERO,
};
let mut facet = ArticulatedFacet::fixed(base);
facet.advance(100.0);
assert_eq!(facet.current_shape(), base.shape);
}
#[test]
fn zero_flow_direction_zero_area() {
let shape = SurfaceShape::FlatPlate {
area: 10.0,
normal: DVec3::X,
};
assert_eq!(shape.projected_area(DVec3::ZERO), 0.0);
assert_eq!(shape.effective_normal(DVec3::ZERO), DVec3::ZERO);
}
#[test]
fn zero_axis_zero_area() {
let cyl = SurfaceShape::Cylinder {
radius: 2.0,
length: 5.0,
axis: DVec3::ZERO,
};
assert_eq!(cyl.projected_area(DVec3::X), 0.0);
assert_eq!(cyl.effective_normal(DVec3::X), DVec3::ZERO);
let fr = SurfaceShape::ConicalFrustum {
radius_bottom: 3.0,
radius_top: 1.0,
length: 4.0,
axis: DVec3::ZERO,
};
assert_eq!(fr.projected_area(DVec3::X), 0.0);
assert_eq!(fr.effective_normal(DVec3::X), DVec3::ZERO);
}
#[test]
fn flow_direction_magnitude_irrelevant() {
let shape = SurfaceShape::Cylinder {
radius: 2.0,
length: 5.0,
axis: DVec3::Y,
};
let a1 = shape.projected_area(DVec3::X);
let a2 = shape.projected_area(DVec3::X * 1000.0);
assert!((a1 - a2).abs() < TOL);
}
}