1pub fn az_dir(az_deg: f32) -> (f32, f32) {
16 let a = az_deg.to_radians();
17 (a.sin(), -a.cos())
18}
19
20#[derive(Debug, Clone, Copy)]
23pub struct RingSegment {
24 pub x: f32,
25 pub z: f32,
26 pub yaw_deg: f32,
27 pub len: f32,
28}
29
30pub fn gate_reach(n_segments: usize, requested_deg: f32) -> f32 {
34 requested_deg.max(0.55 * 360.0 / n_segments.max(1) as f32)
35}
36
37pub fn ring_segments(r: f32, n: usize, gates: &[f32], gate_reach_deg: f32) -> Vec<RingSegment> {
42 let seg_len = 2.0 * r * (std::f32::consts::PI / n.max(1) as f32).sin();
43 (0..n)
44 .filter_map(|k| {
45 let az_mid = (k as f32 + 0.5) / n as f32 * 360.0;
46 if gates.iter().any(|g| ang_dist(az_mid, *g) <= gate_reach_deg) {
47 return None;
48 }
49 let (dx, dz) = az_dir(az_mid);
50 Some(RingSegment {
51 x: dx * r,
52 z: dz * r,
53 yaw_deg: -az_mid,
54 len: seg_len,
55 })
56 })
57 .collect()
58}
59
60pub fn ring_joints(r: f32, n: usize) -> Vec<(f32, f32)> {
62 (0..n)
63 .map(|k| {
64 let (dx, dz) = az_dir(k as f32 / n as f32 * 360.0);
65 (dx * r, dz * r)
66 })
67 .collect()
68}
69
70#[derive(Debug, Clone, Copy)]
80pub struct Corridor {
81 pub az: f32,
82}
83
84impl Corridor {
85 pub fn new(az: f32) -> Self {
86 Corridor { az }
87 }
88
89 pub fn dir(&self) -> (f32, f32) {
91 az_dir(self.az)
92 }
93
94 pub fn side_axis(&self) -> (f32, f32) {
96 let a = self.az.to_radians();
97 (a.cos(), a.sin())
98 }
99
100 pub fn at(&self, r: f32, side: f32, lift: f32) -> [f32; 3] {
102 let (dx, dz) = self.dir();
103 let (sx, sz) = self.side_axis();
104 [dx * r + sx * side, lift, dz * r + sz * side]
105 }
106
107 pub fn yaw_along(&self) -> f32 {
109 90.0 - self.az
110 }
111
112 pub fn yaw_across(&self) -> f32 {
116 -self.az
117 }
118
119 pub fn yaw_face_in(&self, side: f32) -> f32 {
122 if side < 0.0 {
123 90.0 - self.az
124 } else {
125 270.0 - self.az
126 }
127 }
128}
129
130pub fn ang_dist(a: f32, b: f32) -> f32 {
132 let mut d = (a - b).abs() % 360.0;
133 if d > 180.0 {
134 d = 360.0 - d;
135 }
136 d
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn segments_sit_on_the_circle_and_face_tangent() {
145 let ring = ring_segments(10.0, 12, &[], 0.0);
146 assert_eq!(ring.len(), 12);
147 for s in &ring {
148 let r = (s.x * s.x + s.z * s.z).sqrt();
149 assert!((r - 10.0).abs() < 1e-4, "on the circle ({r})");
150 let yaw = s.yaw_deg.to_radians();
152 let tangent = (yaw.cos(), -yaw.sin());
153 let radial = (s.x / r, s.z / r);
154 let dot = tangent.0 * radial.0 + tangent.1 * radial.1;
155 assert!(dot.abs() < 1e-4, "tangent ⊥ radial (dot {dot})");
156 }
157 let expect = 2.0 * 10.0 * (std::f32::consts::PI / 12.0).sin();
159 assert!((ring[0].len - expect).abs() < 1e-4);
160 }
161
162 #[test]
163 fn gates_open_even_on_joints() {
164 let reach = gate_reach(12, 0.0);
167 let ring = ring_segments(10.0, 12, &[0.0], reach);
168 assert_eq!(ring.len(), 10, "two segments removed at the joint gate");
169 let ring = ring_segments(10.0, 12, &[180.0], reach);
172 assert_eq!(ring.len(), 10);
173 }
174
175 #[test]
176 fn corridor_frame_points_and_turns_correctly() {
177 let c = Corridor::new(0.0);
179 let p = c.at(10.0, 2.0, 1.5);
180 assert!(
181 (p[0] - 2.0).abs() < 1e-5 && (p[1] - 1.5).abs() < 1e-5 && (p[2] + 10.0).abs() < 1e-5
182 );
183 let yaw = c.yaw_along().to_radians();
185 let x_axis = (yaw.cos(), -yaw.sin());
186 let dot_dir = x_axis.0 * c.dir().0 + x_axis.1 * c.dir().1;
187 assert!(dot_dir.abs() > 0.999, "along maps X onto dir ({dot_dir})");
188 let yaw = c.yaw_across().to_radians();
190 let x_axis = (yaw.cos(), -yaw.sin());
191 let dot_side = x_axis.0 * c.side_axis().0 + x_axis.1 * c.side_axis().1;
192 assert!(
193 dot_side.abs() > 0.999,
194 "across maps X onto side ({dot_side})"
195 );
196 for (side, sign) in [(-3.0f32, 1.0f32), (3.0, -1.0)] {
198 let yaw = c.yaw_face_in(side).to_radians();
199 let normal = (yaw.sin(), yaw.cos());
200 let dot = normal.0 * c.side_axis().0 + normal.1 * c.side_axis().1;
201 assert!(
202 (dot - sign).abs() < 1e-3,
203 "board at {side} faces in ({dot})"
204 );
205 }
206 let c = Corridor::new(-55.0);
208 let yaw = c.yaw_along().to_radians();
209 let x_axis = (yaw.cos(), -yaw.sin());
210 let dot = x_axis.0 * c.dir().0 + x_axis.1 * c.dir().1;
211 assert!(dot.abs() > 0.999, "along holds at −55° ({dot})");
212 }
213
214 #[test]
215 fn joints_count_and_convention() {
216 let joints = ring_joints(5.0, 8);
217 assert_eq!(joints.len(), 8);
218 assert!((joints[0].0).abs() < 1e-5 && (joints[0].1 + 5.0).abs() < 1e-5);
220 }
221}