Skip to main content

halley_core/
bearings.rs

1use crate::field::{Field, NodeId, Vec2};
2use crate::viewport::Viewport;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum Bearing {
6    N,
7    NE,
8    E,
9    SE,
10    S,
11    SW,
12    W,
13    NW,
14}
15
16impl Bearing {
17    pub fn from_delta(d: Vec2) -> Self {
18        // Angle in radians, -pi..pi, where 0 is +x (east)
19        let a = d.y.atan2(d.x);
20
21        // Split circle into 8 equal wedges (pi/4 each).
22        // We map wedges so the result is intuitive in screen terms with y-down.
23        //
24        // E:  -22.5°..+22.5°
25        // SE: +22.5°..+67.5°
26        // S:  +67.5°..+112.5°
27        // SW: +112.5°..+157.5°
28        // W:  else near +/-180°
29        // NW: -157.5°..-112.5°
30        // N:  -112.5°..-67.5°
31        // NE: -67.5°..-22.5°
32        const PI: f32 = std::f32::consts::PI;
33        const P8: f32 = PI / 8.0;
34
35        if (-P8..=P8).contains(&a) {
36            Bearing::E
37        } else if (P8..=3.0 * P8).contains(&a) {
38            Bearing::SE
39        } else if (3.0 * P8..=5.0 * P8).contains(&a) {
40            Bearing::S
41        } else if (5.0 * P8..=7.0 * P8).contains(&a) {
42            Bearing::SW
43        } else if (-3.0 * P8..=-P8).contains(&a) {
44            Bearing::NE
45        } else if (-5.0 * P8..=-3.0 * P8).contains(&a) {
46            Bearing::N
47        } else if (-7.0 * P8..=-5.0 * P8).contains(&a) {
48            Bearing::NW
49        } else {
50            Bearing::W
51        }
52    }
53}
54
55/// Bearings for all experience-visible nodes that are off-screen.
56/// Returns (NodeId, Bearing).
57pub fn bearings_for_visible_nodes(field: &Field, vp: &Viewport) -> Vec<(NodeId, Bearing)> {
58    field
59        .nodes()
60        .keys()
61        .copied()
62        .filter(|&id| field.participates_in_field_view(id))
63        .filter(|&id| field.is_visible(id))
64        .filter_map(|id| {
65            let n = field.node(id)?;
66            let b = bearing_to_point(vp, n.pos)?;
67            Some((id, b))
68        })
69        .collect()
70}
71
72/// Bearings for all experience-visible *anchor* nodes that are off-screen.
73/// Returns (NodeId, Bearing).
74///
75/// Anchors do NOT bypass visibility rules. If a node is hidden-by-cluster,
76/// explicitly hidden, or detached, it is not in the experience layer and
77/// should not appear in Bearings.
78pub fn bearings_for_anchors(field: &Field, vp: &Viewport) -> Vec<(NodeId, Bearing)> {
79    field
80        .nodes()
81        .iter()
82        .filter_map(|(&id, n)| {
83            if !field.participates_in_field_view(id) {
84                return None;
85            }
86            if !field.is_visible(id) {
87                return None;
88            }
89            if !n.anchor {
90                return None;
91            }
92            let b = bearing_to_point(vp, n.pos)?;
93            Some((id, b))
94        })
95        .collect()
96}
97
98/// Returns the bearing direction from the viewport center to `point`,
99/// but only if the point is off-screen.
100pub fn bearing_to_point(vp: &Viewport, point: Vec2) -> Option<Bearing> {
101    let r = vp.rect();
102    if r.contains(point) {
103        return None;
104    }
105
106    let d = Vec2 {
107        x: point.x - vp.center.x,
108        y: point.y - vp.center.y,
109    };
110
111    Some(Bearing::from_delta(d))
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::field::Vec2;
118
119    #[test]
120    fn inside_viewport_returns_none() {
121        let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
122        assert_eq!(bearing_to_point(&vp, Vec2 { x: 10.0, y: 10.0 }), None);
123    }
124
125    #[test]
126    fn cardinal_directions() {
127        let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
128
129        assert_eq!(
130            bearing_to_point(&vp, Vec2 { x: 1000.0, y: 0.0 }),
131            Some(Bearing::E)
132        );
133        assert_eq!(
134            bearing_to_point(&vp, Vec2 { x: -1000.0, y: 0.0 }),
135            Some(Bearing::W)
136        );
137        assert_eq!(
138            bearing_to_point(&vp, Vec2 { x: 0.0, y: 1000.0 }),
139            Some(Bearing::S)
140        );
141        assert_eq!(
142            bearing_to_point(&vp, Vec2 { x: 0.0, y: -1000.0 }),
143            Some(Bearing::N)
144        );
145    }
146
147    #[test]
148    fn diagonal_directions() {
149        let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
150
151        assert_eq!(
152            bearing_to_point(
153                &vp,
154                Vec2 {
155                    x: 1000.0,
156                    y: 1000.0
157                }
158            ),
159            Some(Bearing::SE)
160        );
161        assert_eq!(
162            bearing_to_point(
163                &vp,
164                Vec2 {
165                    x: -1000.0,
166                    y: 1000.0
167                }
168            ),
169            Some(Bearing::SW)
170        );
171        assert_eq!(
172            bearing_to_point(
173                &vp,
174                Vec2 {
175                    x: 1000.0,
176                    y: -1000.0
177                }
178            ),
179            Some(Bearing::NE)
180        );
181        assert_eq!(
182            bearing_to_point(
183                &vp,
184                Vec2 {
185                    x: -1000.0,
186                    y: -1000.0
187                }
188            ),
189            Some(Bearing::NW)
190        );
191    }
192
193    #[test]
194    fn bearings_skip_hidden_nodes() {
195        use crate::field::{Field, Vec2};
196
197        let mut field = Field::new();
198        let a = field.spawn_surface("A", Vec2 { x: 1000.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
199        let b = field.spawn_surface("B", Vec2 { x: -1000.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
200
201        assert!(field.set_hidden(b, true));
202
203        let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
204
205        let bs = bearings_for_visible_nodes(&field, &vp);
206
207        assert_eq!(bs.len(), 1);
208        assert_eq!(bs[0].0, a);
209        assert_eq!(bs[0].1, Bearing::E);
210    }
211
212    #[test]
213    fn bearings_for_anchors_only_includes_anchors() {
214        use crate::field::{Field, Vec2};
215
216        let mut field = Field::new();
217        let a = field.spawn_surface("A", Vec2 { x: 1000.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
218        let b = field.spawn_surface("B", Vec2 { x: -1000.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
219
220        assert!(field.set_anchor(b, true));
221
222        let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
223
224        let bs = bearings_for_anchors(&field, &vp);
225
226        assert_eq!(bs.len(), 1);
227        assert_eq!(bs[0].0, b);
228        assert_eq!(bs[0].1, Bearing::W);
229
230        // ensure non-anchor isn't included
231        assert_ne!(a, b);
232    }
233
234    #[test]
235    fn bearings_for_anchors_skips_hidden_anchors() {
236        use crate::field::{Field, Vec2};
237
238        let mut field = Field::new();
239        let a = field.spawn_surface("A", Vec2 { x: 1000.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
240
241        assert!(field.set_anchor(a, true));
242        assert!(field.set_hidden(a, true));
243
244        let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 100.0 });
245
246        let bs = bearings_for_anchors(&field, &vp);
247        assert!(bs.is_empty());
248    }
249}