Skip to main content

halley_core/
decay.rs

1use crate::field::{Field, NodeId, NodeKind, Vec2};
2use crate::viewport::{FocusRing, FocusZone, Viewport};
3
4#[cfg(test)]
5use crate::field::NodeState;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum DecayLevel {
9    Hot,  // Active
10    Cold, // Node
11}
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct DecayPolicy {
15    /// Age >= node_after_ms => Cold/Node
16    pub node_after_ms: u64,
17}
18
19impl DecayPolicy {
20    pub fn new(node_after_ms: u64) -> Self {
21        Self { node_after_ms }
22    }
23}
24
25/// Advance representation decay for all nodes based on time since last touch.
26/// - `now_ms` is a monotonic ms counter controlled by the outer loop.
27/// - `focused` is pinned Hot.
28/// - Core nodes do not decay (they remain handles).
29pub fn tick_decay(field: &mut Field, now_ms: u64, policy: DecayPolicy, focused: Option<NodeId>) {
30    let ids: Vec<NodeId> = field.nodes().keys().copied().collect();
31
32    for id in ids {
33        let Some(n) = field.node(id) else { continue };
34
35        if n.kind == NodeKind::Core {
36            continue;
37        }
38
39        if field.cluster_id_for_member_public(id).is_some() {
40            continue;
41        }
42
43        if Some(id) == focused {
44            let _ = field.set_decay_level(id, DecayLevel::Hot);
45            continue;
46        }
47
48        let age = now_ms.saturating_sub(n.last_touch_ms);
49
50        if age >= policy.node_after_ms {
51            let _ = field.set_decay_level(id, DecayLevel::Cold);
52        } else {
53            let _ = field.set_decay_level(id, DecayLevel::Hot);
54        }
55    }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq)]
59pub struct FocusRingDecayPolicy {
60    /// Inside the focus ring:
61    /// - age < inside_to_node_ms => Hot/Active
62    /// - otherwise => Cold/Node
63    pub inside_to_node_ms: u64,
64
65    /// Outside the focus ring:
66    /// - if true => immediately Cold/Node
67    pub outside_immediate_cold: bool,
68}
69
70impl FocusRingDecayPolicy {
71    pub fn new() -> Self {
72        Self {
73            inside_to_node_ms: 1_200_000,
74            outside_immediate_cold: true,
75        }
76    }
77}
78
79impl Default for FocusRingDecayPolicy {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85/// Focus-ring-aware decay:
86/// - Inside focus ring: Hot, then Node based on timer
87/// - Outside focus ring: Cold immediately
88/// - Focused node: Hot
89/// - Core nodes do not decay
90pub fn tick_decay_focus_ring(
91    field: &mut Field,
92    vp: &Viewport,
93    now_ms: u64,
94    focus_ring: FocusRing,
95    policy: FocusRingDecayPolicy,
96    focused: Option<NodeId>,
97) {
98    let ids: Vec<NodeId> = field.nodes().keys().copied().collect();
99
100    for id in ids {
101        let (kind, pos, active_extent, last_touch_ms) = {
102            let Some(n) = field.node(id) else { continue };
103            (n.kind.clone(), n.pos, n.footprint, n.last_touch_ms)
104        };
105
106        if kind == NodeKind::Core {
107            continue;
108        }
109
110        if field.cluster_id_for_member_public(id).is_some() {
111            continue;
112        }
113
114        if Some(id) == focused {
115            let _ = field.set_decay_level(id, DecayLevel::Hot);
116            continue;
117        }
118
119        let zone = dominant_focus_zone(focus_ring, vp.center, pos, active_extent);
120
121        match zone {
122            FocusZone::Inside => {
123                let age = now_ms.saturating_sub(last_touch_ms);
124                if age >= policy.inside_to_node_ms {
125                    let _ = field.set_decay_level(id, DecayLevel::Cold);
126                } else {
127                    let _ = field.set_decay_level(id, DecayLevel::Hot);
128                }
129            }
130            FocusZone::Outside => {
131                if policy.outside_immediate_cold {
132                    let _ = field.set_decay_level(id, DecayLevel::Cold);
133                } else {
134                    let age = now_ms.saturating_sub(last_touch_ms);
135                    if age >= policy.inside_to_node_ms {
136                        let _ = field.set_decay_level(id, DecayLevel::Cold);
137                    } else {
138                        let _ = field.set_decay_level(id, DecayLevel::Hot);
139                    }
140                }
141            }
142        }
143    }
144}
145
146fn dominant_focus_zone(
147    focus_ring: FocusRing,
148    vp_center: Vec2,
149    pos: Vec2,
150    footprint: Vec2,
151) -> FocusZone {
152    let w = footprint.x.abs();
153    let h = footprint.y.abs();
154
155    if w < 1.0 || h < 1.0 {
156        return focus_ring.zone(vp_center, pos);
157    }
158
159    let sx = 5usize;
160    let sy = 5usize;
161    let mut inside = 0usize;
162
163    let min_x = pos.x - w * 0.5;
164    let min_y = pos.y - h * 0.5;
165
166    for iy in 0..sy {
167        for ix in 0..sx {
168            let tx = (ix as f32 + 0.5) / sx as f32;
169            let ty = (iy as f32 + 0.5) / sy as f32;
170            let p = Vec2 {
171                x: min_x + tx * w,
172                y: min_y + ty * h,
173            };
174
175            if focus_ring.zone(vp_center, p) == FocusZone::Inside {
176                inside += 1;
177            }
178        }
179    }
180
181    let total = (sx * sy) as f32;
182    let frac_inside = inside as f32 / total;
183
184    if frac_inside > 0.5 {
185        FocusZone::Inside
186    } else {
187        FocusZone::Outside
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use crate::field::Vec2;
195
196    fn default_focus_ring() -> FocusRing {
197        FocusRing::new(50.0, 30.0, 0.0, 0.0)
198    }
199
200    #[test]
201    fn decays_hot_to_cold() {
202        let mut f = Field::new();
203        let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
204
205        assert!(f.touch(a, 0));
206        assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
207        assert_eq!(f.node(a).unwrap().state, NodeState::Active);
208
209        let policy = DecayPolicy::new(5000);
210
211        tick_decay(&mut f, 1500, policy, None);
212        assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
213        assert_eq!(f.node(a).unwrap().state, NodeState::Active);
214
215        tick_decay(&mut f, 6000, policy, None);
216        assert_eq!(f.node(a).unwrap().decay, DecayLevel::Cold);
217        assert_eq!(f.node(a).unwrap().state, NodeState::Node);
218    }
219
220    #[test]
221    fn focused_node_stays_hot() {
222        let mut f = Field::new();
223        let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
224        assert!(f.touch(a, 0));
225
226        let policy = DecayPolicy::new(5000);
227
228        tick_decay(&mut f, 6000, policy, Some(a));
229        assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
230        assert_eq!(f.node(a).unwrap().state, NodeState::Active);
231    }
232
233    #[test]
234    fn core_does_not_decay() {
235        let mut f = Field::new();
236        let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
237        let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
238
239        let cid = f.create_cluster(vec![a, b]).unwrap();
240        let core = f.collapse_cluster(cid).unwrap();
241
242        let policy = DecayPolicy::new(5000);
243        tick_decay(&mut f, 999_999, policy, None);
244
245        let n = f.node(core).unwrap();
246        assert_eq!(n.kind, NodeKind::Core);
247        assert_eq!(n.state, NodeState::Core);
248    }
249
250    #[test]
251    fn clustered_members_do_not_decay() {
252        let mut f = Field::new();
253        let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
254        let b = f.spawn_surface("B", Vec2 { x: 10.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
255
256        let cid = f.create_cluster(vec![a, b]).unwrap();
257        let _ = f.collapse_cluster(cid);
258
259        let policy = DecayPolicy::new(1);
260        tick_decay(&mut f, 999_999, policy, None);
261
262        assert_eq!(f.node(a).unwrap().state, NodeState::Node);
263        assert_eq!(f.node(b).unwrap().state, NodeState::Node);
264        assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
265        assert_eq!(f.node(b).unwrap().decay, DecayLevel::Hot);
266    }
267
268    #[test]
269    fn inside_focus_ring_near_center_stays_hot() {
270        let mut f = Field::new();
271        let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
272        assert!(f.touch(a, 0));
273
274        let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
275        let ring = default_focus_ring();
276        let policy = FocusRingDecayPolicy::new();
277
278        tick_decay_focus_ring(&mut f, &vp, 999_999, ring, policy, None);
279
280        assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
281        assert_eq!(f.node(a).unwrap().state, NodeState::Active);
282    }
283
284    #[test]
285    fn inside_focus_ring_stays_hot_before_threshold() {
286        let mut f = Field::new();
287        let a = f.spawn_surface("A", Vec2 { x: 49.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
288        assert!(f.touch(a, 0));
289
290        let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
291        let ring = default_focus_ring();
292        let mut policy = FocusRingDecayPolicy::new();
293        policy.inside_to_node_ms = 5000;
294
295        tick_decay_focus_ring(&mut f, &vp, 1500, ring, policy, None);
296
297        assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
298        assert_eq!(f.node(a).unwrap().state, NodeState::Active);
299    }
300
301    #[test]
302    fn inside_focus_ring_can_decay_to_cold() {
303        let mut f = Field::new();
304        let a = f.spawn_surface("A", Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
305        assert!(f.touch(a, 0));
306
307        let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
308        let ring = default_focus_ring();
309        let mut policy = FocusRingDecayPolicy::new();
310        policy.inside_to_node_ms = 5000;
311
312        tick_decay_focus_ring(&mut f, &vp, 7000, ring, policy, None);
313
314        assert_eq!(f.node(a).unwrap().decay, DecayLevel::Cold);
315        assert_eq!(f.node(a).unwrap().state, NodeState::Node);
316    }
317
318    #[test]
319    fn outside_focus_ring_goes_cold_immediately() {
320        let mut f = Field::new();
321        let a = f.spawn_surface("A", Vec2 { x: 500.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
322        assert!(f.touch(a, 0));
323
324        let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
325        let ring = default_focus_ring();
326        let policy = FocusRingDecayPolicy::new();
327
328        tick_decay_focus_ring(&mut f, &vp, 1000, ring, policy, None);
329
330        assert_eq!(f.node(a).unwrap().decay, DecayLevel::Cold);
331        assert_eq!(f.node(a).unwrap().state, NodeState::Node);
332    }
333
334    #[test]
335    fn focused_node_stays_hot_with_focus_ring_policy() {
336        let mut f = Field::new();
337        let a = f.spawn_surface("A", Vec2 { x: 500.0, y: 0.0 }, Vec2 { x: 10.0, y: 10.0 });
338        assert!(f.touch(a, 0));
339
340        let vp = Viewport::new(Vec2 { x: 0.0, y: 0.0 }, Vec2 { x: 100.0, y: 50.0 });
341        let ring = default_focus_ring();
342        let policy = FocusRingDecayPolicy::new();
343
344        tick_decay_focus_ring(&mut f, &vp, 999_999, ring, policy, Some(a));
345
346        assert_eq!(f.node(a).unwrap().decay, DecayLevel::Hot);
347        assert_eq!(f.node(a).unwrap().state, NodeState::Active);
348    }
349}