bevy_map_camera 0.5.0

3D Camera Controller for Bevy
Documentation
use super::{CameraController, ray_from_screenspace};
use bevy_app::{App, Plugin, PreUpdate};
use bevy_camera::Camera;
use bevy_ecs::prelude::*;
use bevy_input::touch::Touches;
use bevy_log::{error, warn};
use bevy_math::{Dir3, Ray3d, Vec2, Vec3, primitives::InfinitePlane3d};
use bevy_picking::{
    backend::ray::{RayId, RayMap},
    pointer::PointerId,
};
use bevy_platform::collections::HashMap;
use bevy_transform::components::GlobalTransform;
use bevy_window::{PrimaryWindow, Window};

use crate::{CameraChange, LookTransform};

use super::{
    CameraControllerSettings, ControlMessage,
    touch_inputs::{Pinch, TouchInputSettings, TouchInputs},
};

pub(super) struct TouchInputPlugin;

impl Plugin for TouchInputPlugin {
    fn build(&self, app: &mut App) {
        app.register_type::<TouchInputSettings>();
        app.init_resource::<TouchInputSettings>();
        app.add_systems(
            PreUpdate,
            (zoom_orbit_camera, grab_pan, rotate_orbit_camera).in_set(CameraChange::Before),
        );
    }
}

/// Handles the zooming of the orbital camera
fn zoom_orbit_camera(
    mut touches: TouchInputs,
    cam_q: Single<(&Camera, &GlobalTransform, &LookTransform, &CameraController)>,
    main_window: Single<&Window, With<PrimaryWindow>>,
    settings: Res<CameraControllerSettings>,
    mut camera_writer: MessageWriter<ControlMessage>,
) {
    // Get the deltas of the two touches
    let Some(Pinch {
        distance_delta,
        middle,
    }) = touches.get_pinch()
    else {
        return;
    };

    let (camera, camera_gt, camera_lt, controller) = cam_q.into_inner();
    let window = main_window.into_inner();

    let scalar = 1.0 - distance_delta * settings.touch_zoom_sensitivity_modifier;

    if scalar == 1.0 {
        return;
    }

    let Ok(ray) = ray_from_screenspace(middle, camera, camera_gt, window) else {
        camera_writer.write(ControlMessage::Zoom {
            zoom_scalar: scalar,
            zoom_target: camera_lt.target,
        });
        error!("Unable to create ray from screenspace");
        return;
    };

    let Some(target_distance) = ray.intersect_plane(
        Vec3::Y * controller.grab_height,
        InfinitePlane3d { normal: Dir3::Y },
    ) else {
        warn!("Cursor click did not intersect with Grab plane");
        return;
    };

    let target = ray.get_point(target_distance);

    camera_writer.write(ControlMessage::Zoom {
        zoom_scalar: scalar,
        zoom_target: target,
    });
}

/// Handles the rotation of the orbital camera, dont run in orthographic
fn rotate_orbit_camera(
    mut touches: TouchInputs,
    settings: Res<CameraControllerSettings>,
    mut camera_writer: MessageWriter<ControlMessage>,
) {
    let Some(rotation_move) = touches.get_two_touch_drag() else {
        return;
    };
    camera_writer.write(ControlMessage::Orbit(
        rotation_move * settings.touch_rotation_sensitivity_modifier,
    ));
}

fn grab_pan(
    cam_q: Single<(Entity, &CameraController), With<Camera>>,
    mut inputs: TouchInputs,
    touches: Res<Touches>,
    mut first_ray_hit: Local<Option<Vec3>>,
    mut camera_writer: MessageWriter<ControlMessage>,
    mut over_threshold: Local<bool>,
    mut first_screen_touch: Local<Option<Vec2>>,
    ray_map: Res<RayMap>,
) {
    let (camera_entity, controller) = cam_q.into_inner();

    let intersection = get_plane_intersection_point(controller, &ray_map.map, camera_entity).map(
        |(pointer_id, point)| {
            (
                pointer_id
                    .get_touch_id()
                    .and_then(|id| touches.get_pressed(id).map(|touch| touch.position())),
                point,
            )
        },
    );

    if let Err(TouchIntersectionPointError::NoIntersection) = intersection {
        warn!("Touch Grab pan intersection did not intersect with Grab plane");
    }

    if first_ray_hit.is_none() {
        match intersection {
            Ok((Some(touch_pos), intersection_point)) => {
                *first_ray_hit = Some(intersection_point);
                *over_threshold = false;

                *first_screen_touch = Some(touch_pos);
            }
            Ok((None, _)) => {
                warn!("Tried to start Touch grab pan but no touch position was found");
            }
            _ => {}
        }
    }

    if (intersection == Err(TouchIntersectionPointError::NoTouchRay)
        || intersection == Err(TouchIntersectionPointError::MultipleTouchRays))
        && first_ray_hit.is_some()
    {
        *first_ray_hit = None;
        inputs.clear_last_touches();
    }

    if let (Ok((Some(touch_pos), point)), Some(first_hit), Some(screen_touch)) =
        (intersection, *first_ray_hit, *first_screen_touch)
    {
        let first_hit_diff = first_hit - point;

        if touch_pos.distance(screen_touch) > 3.0 || *over_threshold {
            *over_threshold = true;
            camera_writer.write(ControlMessage::TranslateTarget(first_hit_diff));
        }
    }
}

fn get_plane_intersection_point(
    controller: &CameraController,
    ray_map: &HashMap<RayId, Ray3d>,
    camera_entity: Entity,
) -> Result<(PointerId, Vec3), TouchIntersectionPointError> {
    let mut filtered_map = ray_map
        .iter()
        .filter(|(ray_id, _)| ray_id.pointer.is_touch() && ray_id.camera == camera_entity)
        .map(|(ray_id, ray)| (ray_id.pointer, ray))
        .collect::<Vec<_>>();

    if filtered_map.len() > 1 {
        // Multiple touches
        return Err(TouchIntersectionPointError::MultipleTouchRays);
    }

    let (pointer_id, ray) = filtered_map
        .pop()
        .ok_or(TouchIntersectionPointError::NoTouchRay)?;

    ray.intersect_plane(
        Vec3::Y * controller.grab_height,
        InfinitePlane3d { normal: Dir3::Y },
    )
    .map(|distance: f32| (pointer_id, ray.get_point(distance)))
    .ok_or(TouchIntersectionPointError::NoIntersection)
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum TouchIntersectionPointError {
    NoIntersection,
    MultipleTouchRays,
    NoTouchRay,
}