Skip to main content

mittens_engine/engine/ecs/system/
pointer_system.rs

1use crate::engine::ecs::component::{
2    ControllerXRComponent, InputComponent, InputXRComponent, PointerComponent, RayCastComponent,
3};
4use crate::engine::ecs::system::XrInputState;
5use crate::engine::ecs::{ComponentId, SignalEmitter, World};
6use crate::engine::user_input::InputState;
7use std::collections::HashMap;
8use winit::event::MouseButton;
9
10/// Runtime owner for pointer-specific state and lifecycle.
11///
12/// Initial responsibilities:
13/// - ensure each `PointerComponent` owns a child `RayCastComponent`
14/// - cache pointer ↔ raycaster relationships
15///
16/// Higher-level pointer behavior (topology classification, trigger policy, etc.) will migrate
17/// here incrementally from other systems.
18#[derive(Debug, Default)]
19pub struct PointerSystem {
20    pointer_to_raycast: HashMap<ComponentId, ComponentId>,
21}
22
23impl PointerSystem {
24    pub fn register_pointer(
25        &mut self,
26        world: &mut World,
27        component: ComponentId,
28        emit: &mut dyn SignalEmitter,
29    ) {
30        let Some(pointer) = world.get_component_by_id_as::<PointerComponent>(component) else {
31            return;
32        };
33
34        if !pointer.enabled {
35            self.pointer_to_raycast.remove(&component);
36            return;
37        }
38
39        let existing_raycast = world.children_of(component).iter().copied().find(|&child| {
40            world
41                .get_component_by_id_as::<RayCastComponent>(child)
42                .is_some()
43        });
44
45        let raycast = match existing_raycast {
46            Some(raycast) => raycast,
47            None => {
48                let raycast = world.add_component(RayCastComponent::event_driven());
49                if world.add_child(component, raycast).is_err() {
50                    return;
51                }
52                world.init_component_tree(raycast, emit);
53                raycast
54            }
55        };
56
57        self.pointer_to_raycast.insert(component, raycast);
58    }
59
60    pub fn remove_pointer(&mut self, component: ComponentId) {
61        self.pointer_to_raycast.remove(&component);
62    }
63
64    pub fn raycast_for_pointer(&self, component: ComponentId) -> Option<ComponentId> {
65        self.pointer_to_raycast.get(&component).copied()
66    }
67
68    pub fn raycast_to_pointer(&self, raycaster: ComponentId) -> Option<ComponentId> {
69        self.pointer_to_raycast
70            .iter()
71            .find(|(_, rc)| **rc == raycaster)
72            .map(|(&ptr, _)| ptr)
73    }
74}
75
76/// Lineage classification for a pointer / raycaster node.
77///
78/// Tells callers what kind of ancestor drives pose and what trigger source applies.
79#[derive(Debug, Clone, Copy, Default)]
80pub struct PointerTopologyContext {
81    pub has_desktop_input_driver: bool,
82    pub has_xr_input_driver: bool,
83    pub has_controller_driver: bool,
84    pub has_desktop_camera_anchor: bool,
85    pub has_xr_camera_anchor: bool,
86}
87
88/// Walk ancestors looking for a component of type `T`.
89pub fn has_ancestor_component<T: 'static>(world: &World, start: ComponentId) -> bool {
90    let mut cur = start;
91    while let Some(parent) = world.parent_of(cur) {
92        if world.get_component_by_id_as::<T>(parent).is_some() {
93            return true;
94        }
95        cur = parent;
96    }
97    false
98}
99
100/// Find the nearest ancestor (or self) that is a `TransformComponent`.
101pub fn nearest_ancestor_transform(world: &World, start: ComponentId) -> Option<ComponentId> {
102    if world
103        .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(start)
104        .is_some()
105    {
106        return Some(start);
107    }
108    let mut cur = start;
109    while let Some(parent) = world.parent_of(cur) {
110        if world
111            .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(parent)
112            .is_some()
113        {
114            return Some(parent);
115        }
116        cur = parent;
117    }
118    None
119}
120
121/// Classify a pointer node's lineage.
122///
123/// Camera anchor flags are set when a Camera3D/2D (desktop) or CameraXR is an **ancestor** of
124/// the pointer — i.e. `Pointer` is a child/descendant of the camera component.
125pub fn pointer_topology_context(world: &World, cid: ComponentId) -> PointerTopologyContext {
126    PointerTopologyContext {
127        has_desktop_input_driver: has_ancestor_component::<InputComponent>(world, cid),
128        has_xr_input_driver: has_ancestor_component::<InputXRComponent>(world, cid),
129        has_controller_driver: has_ancestor_component::<ControllerXRComponent>(world, cid),
130        has_desktop_camera_anchor: has_ancestor_component::<
131            crate::engine::ecs::component::Camera3DComponent,
132        >(world, cid)
133            || has_ancestor_component::<crate::engine::ecs::component::Camera2DComponent>(
134                world, cid,
135            ),
136        has_xr_camera_anchor: has_ancestor_component::<
137            crate::engine::ecs::component::CameraXRComponent,
138        >(world, cid),
139    }
140}
141
142/// Which `PointerComponent` ids have an active trigger state this frame.
143///
144/// Built by `PointerSystem::build_activations` from `InputState` (mouse) and `XrInputState`
145/// (XR controllers). `GestureSystem` consumes this without knowing the underlying input source.
146#[derive(Default, Debug, Clone)]
147pub struct PointerActivations {
148    pub pressed: Vec<ComponentId>,
149    pub down: Vec<ComponentId>,
150    pub released: Vec<ComponentId>,
151}
152
153impl PointerSystem {
154    /// Map each registered pointer to its trigger state this frame.
155    ///
156    /// Desktop pointers use `InputState` mouse left button. Controller-backed pointers use
157    /// `XrInputState` indexed by hand. A gaze-style pointer beneath `CameraXR` has no implicit
158    /// trigger source and must not consume desktop mouse input merely because it lacks a
159    /// `ControllerXR` ancestor.
160    pub fn build_activations(
161        &self,
162        world: &World,
163        input: &InputState,
164        xr: &XrInputState,
165    ) -> PointerActivations {
166        let mut act = PointerActivations::default();
167
168        for (&pointer_cid, _) in &self.pointer_to_raycast {
169            let topo = pointer_topology_context(world, pointer_cid);
170
171            if topo.has_controller_driver {
172                // Resolve which hand owns this pointer's controller ancestor.
173                let hand_idx = controller_hand_index(world, pointer_cid);
174                let Some(i) = hand_idx else { continue };
175                if xr.trigger_pressed[i] {
176                    act.pressed.push(pointer_cid);
177                }
178                if xr.trigger_down[i] {
179                    act.down.push(pointer_cid);
180                }
181                if xr.trigger_released[i] {
182                    act.released.push(pointer_cid);
183                }
184            } else if !topo.has_xr_camera_anchor && !topo.has_xr_input_driver {
185                // Desktop or otherwise non-XR pointer: left mouse button.
186                if input.mouse_pressed.contains(&MouseButton::Left) {
187                    act.pressed.push(pointer_cid);
188                }
189                if input.mouse_down.contains(&MouseButton::Left) {
190                    act.down.push(pointer_cid);
191                }
192                if input.mouse_released.contains(&MouseButton::Left) {
193                    act.released.push(pointer_cid);
194                }
195            }
196        }
197
198        act
199    }
200}
201
202/// Find the nearest ancestor `ControllerXRComponent` and return its hand index (0=left, 1=right).
203fn controller_hand_index(world: &World, start: ComponentId) -> Option<usize> {
204    let mut cur = start;
205    loop {
206        if let Some(c) = world.get_component_by_id_as::<ControllerXRComponent>(cur) {
207            return Some(match c.hand {
208                crate::engine::ecs::component::ControllerHand::Left => 0,
209                crate::engine::ecs::component::ControllerHand::Right => 1,
210            });
211        }
212        cur = world.parent_of(cur)?;
213    }
214}