pub fn az_dir(az_deg: f32) -> (f32, f32) {
let a = az_deg.to_radians();
(a.sin(), -a.cos())
}
#[derive(Debug, Clone, Copy)]
pub struct RingSegment {
pub x: f32,
pub z: f32,
pub yaw_deg: f32,
pub len: f32,
}
pub fn gate_reach(n_segments: usize, requested_deg: f32) -> f32 {
requested_deg.max(0.55 * 360.0 / n_segments.max(1) as f32)
}
pub fn ring_segments(r: f32, n: usize, gates: &[f32], gate_reach_deg: f32) -> Vec<RingSegment> {
let seg_len = 2.0 * r * (std::f32::consts::PI / n.max(1) as f32).sin();
(0..n)
.filter_map(|k| {
let az_mid = (k as f32 + 0.5) / n as f32 * 360.0;
if gates.iter().any(|g| ang_dist(az_mid, *g) <= gate_reach_deg) {
return None;
}
let (dx, dz) = az_dir(az_mid);
Some(RingSegment {
x: dx * r,
z: dz * r,
yaw_deg: -az_mid,
len: seg_len,
})
})
.collect()
}
pub fn ring_joints(r: f32, n: usize) -> Vec<(f32, f32)> {
(0..n)
.map(|k| {
let (dx, dz) = az_dir(k as f32 / n as f32 * 360.0);
(dx * r, dz * r)
})
.collect()
}
#[derive(Debug, Clone, Copy)]
pub struct Corridor {
pub az: f32,
}
impl Corridor {
pub fn new(az: f32) -> Self {
Corridor { az }
}
pub fn dir(&self) -> (f32, f32) {
az_dir(self.az)
}
pub fn side_axis(&self) -> (f32, f32) {
let a = self.az.to_radians();
(a.cos(), a.sin())
}
pub fn at(&self, r: f32, side: f32, lift: f32) -> [f32; 3] {
let (dx, dz) = self.dir();
let (sx, sz) = self.side_axis();
[dx * r + sx * side, lift, dz * r + sz * side]
}
pub fn yaw_along(&self) -> f32 {
90.0 - self.az
}
pub fn yaw_across(&self) -> f32 {
-self.az
}
pub fn yaw_face_in(&self, side: f32) -> f32 {
if side < 0.0 {
90.0 - self.az
} else {
270.0 - self.az
}
}
}
pub fn ang_dist(a: f32, b: f32) -> f32 {
let mut d = (a - b).abs() % 360.0;
if d > 180.0 {
d = 360.0 - d;
}
d
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn segments_sit_on_the_circle_and_face_tangent() {
let ring = ring_segments(10.0, 12, &[], 0.0);
assert_eq!(ring.len(), 12);
for s in &ring {
let r = (s.x * s.x + s.z * s.z).sqrt();
assert!((r - 10.0).abs() < 1e-4, "on the circle ({r})");
let yaw = s.yaw_deg.to_radians();
let tangent = (yaw.cos(), -yaw.sin());
let radial = (s.x / r, s.z / r);
let dot = tangent.0 * radial.0 + tangent.1 * radial.1;
assert!(dot.abs() < 1e-4, "tangent ⊥ radial (dot {dot})");
}
let expect = 2.0 * 10.0 * (std::f32::consts::PI / 12.0).sin();
assert!((ring[0].len - expect).abs() < 1e-4);
}
#[test]
fn gates_open_even_on_joints() {
let reach = gate_reach(12, 0.0);
let ring = ring_segments(10.0, 12, &[0.0], reach);
assert_eq!(ring.len(), 10, "two segments removed at the joint gate");
let ring = ring_segments(10.0, 12, &[180.0], reach);
assert_eq!(ring.len(), 10);
}
#[test]
fn corridor_frame_points_and_turns_correctly() {
let c = Corridor::new(0.0);
let p = c.at(10.0, 2.0, 1.5);
assert!(
(p[0] - 2.0).abs() < 1e-5 && (p[1] - 1.5).abs() < 1e-5 && (p[2] + 10.0).abs() < 1e-5
);
let yaw = c.yaw_along().to_radians();
let x_axis = (yaw.cos(), -yaw.sin());
let dot_dir = x_axis.0 * c.dir().0 + x_axis.1 * c.dir().1;
assert!(dot_dir.abs() > 0.999, "along maps X onto dir ({dot_dir})");
let yaw = c.yaw_across().to_radians();
let x_axis = (yaw.cos(), -yaw.sin());
let dot_side = x_axis.0 * c.side_axis().0 + x_axis.1 * c.side_axis().1;
assert!(
dot_side.abs() > 0.999,
"across maps X onto side ({dot_side})"
);
for (side, sign) in [(-3.0f32, 1.0f32), (3.0, -1.0)] {
let yaw = c.yaw_face_in(side).to_radians();
let normal = (yaw.sin(), yaw.cos());
let dot = normal.0 * c.side_axis().0 + normal.1 * c.side_axis().1;
assert!(
(dot - sign).abs() < 1e-3,
"board at {side} faces in ({dot})"
);
}
let c = Corridor::new(-55.0);
let yaw = c.yaw_along().to_radians();
let x_axis = (yaw.cos(), -yaw.sin());
let dot = x_axis.0 * c.dir().0 + x_axis.1 * c.dir().1;
assert!(dot.abs() > 0.999, "along holds at −55° ({dot})");
}
#[test]
fn joints_count_and_convention() {
let joints = ring_joints(5.0, 8);
assert_eq!(joints.len(), 8);
assert!((joints[0].0).abs() < 1e-5 && (joints[0].1 + 5.0).abs() < 1e-5);
}
}