ambient_input/
picking.rs

1use ambient_core::{
2    camera::{get_active_camera, clip_space_ray},
3    player::{local_user_id, user_id},
4    transform::local_to_world,
5    ui_scene,
6    window::{cursor_position, window_logical_size},
7};
8use ambient_ecs::{components, query, Debuggable, Entity, EntityId, MaybeResource, SystemGroup};
9use ambient_std::shapes::{RayIntersectable, AABB};
10use glam::Vec2;
11
12pub use ambient_ecs::generated::components::core::input::{mouse_over, mouse_pickable_max, mouse_pickable_min};
13
14components!("input", {
15    @[MaybeResource, Debuggable]
16    picker_intersecting: Option<PickerIntersection>,
17
18    @[Debuggable]
19    mouse_pickable: AABB,
20});
21
22#[derive(Debug, Clone, Copy)]
23pub struct PickerIntersection {
24    pub entity: EntityId,
25    pub distance: f32,
26}
27
28pub fn resources() -> Entity {
29    Entity::new().with_default(picker_intersecting())
30}
31
32pub fn frame_systems() -> SystemGroup {
33    SystemGroup::new(
34        "picking",
35        vec![
36            query((mouse_pickable_min().changed(), mouse_pickable_max().changed())).to_system(|q, world, qs, _| {
37                for (id, (min, max)) in q.collect_cloned(world, qs) {
38                    world.add_component(id, mouse_pickable(), AABB { min, max }).unwrap();
39                }
40            }),
41            query((window_logical_size(), cursor_position())).to_system(|q, world, qs, _| {
42                for (id, (window_size, mouse_position)) in q.collect_cloned(world, qs) {
43                    let mut mouse_origin = -Vec2::ONE + (mouse_position / window_size.as_vec2()) * 2.;
44                    mouse_origin.y = -mouse_origin.y;
45                    let camera = match get_active_camera(
46                        world,
47                        ui_scene(),
48                        world.get_ref(id, user_id()).ok().or_else(|| world.resource_opt(local_user_id())),
49                    ) {
50                        Some(cam) => cam,
51                        None => return,
52                    };
53                    let ray = clip_space_ray(world, camera, mouse_origin).unwrap_or_default();
54
55                    let prev_intersecting = world.get(id, picker_intersecting()).unwrap_or_default();
56
57                    let mut intersecting: Option<PickerIntersection> = None;
58                    for (id2, (pickable, local_to_world)) in query((mouse_pickable(), local_to_world())).iter(world, None) {
59                        if local_to_world.is_nan() {
60                            continue;
61                        }
62                        let ray = ray.transform(local_to_world.inverse());
63                        if let Some(dist) = pickable.ray_intersect(ray) {
64                            if intersecting.is_none() || dist < intersecting.as_ref().unwrap().distance {
65                                intersecting = Some(PickerIntersection { entity: id2, distance: dist });
66                            }
67                        }
68                    }
69                    let prev_intersecting_entity = prev_intersecting.map(|x| x.entity);
70                    let intersecting_entity = intersecting.map(|x| x.entity);
71                    if prev_intersecting_entity != intersecting_entity {
72                        if let Some(prev) = prev_intersecting_entity {
73                            if let Ok(prev_mouse_over) = world.get(prev, mouse_over()) {
74                                if prev_mouse_over > 0 {
75                                    world.add_component(prev, mouse_over(), prev_mouse_over - 1).unwrap();
76                                }
77                            }
78                        }
79                        if let Some(new) = intersecting_entity {
80                            let new_mouse_over = world.get(new, mouse_over()).unwrap_or_default();
81                            world.add_component(new, mouse_over(), new_mouse_over + 1).unwrap();
82                        }
83                    }
84                    world.add_component(id, picker_intersecting(), intersecting).unwrap();
85                }
86            }),
87        ],
88    )
89}