use bevy::prelude::*;
#[derive(Debug, Clone, Copy, Default, Component, Reflect)]
pub struct GyroData {
pub pitch: f32,
pub yaw: f32,
pub roll: f32,
pub valid: bool,
}
impl GyroData {
#[must_use]
pub const fn new(pitch: f32, yaw: f32, roll: f32) -> Self {
Self {
pitch,
yaw,
roll,
valid: true,
}
}
pub fn set_raw(&mut self, pitch: f32, yaw: f32, roll: f32) {
self.pitch = pitch;
self.yaw = yaw;
self.roll = roll;
self.valid = true;
}
#[must_use]
pub fn magnitude(&self) -> f32 {
(self.pitch * self.pitch + self.yaw * self.yaw + self.roll * self.roll).sqrt()
}
#[must_use]
pub fn exceeds_threshold(&self, threshold: f32) -> bool {
self.magnitude() > threshold
}
#[cfg(feature = "motion-backends")]
pub fn update_from_backend(&mut self, data: &crate::motion::backend::MotionData) {
self.set_raw(data.gyro_pitch, data.gyro_yaw, data.gyro_roll);
}
}
#[derive(Debug, Clone, Copy, Default, Component, Reflect)]
pub struct AccelData {
pub x: f32,
pub y: f32,
pub z: f32,
pub valid: bool,
}
impl AccelData {
#[must_use]
pub const fn new(x: f32, y: f32, z: f32) -> Self {
Self {
x,
y,
z,
valid: true,
}
}
pub fn set_raw(&mut self, x: f32, y: f32, z: f32) {
self.x = x;
self.y = y;
self.z = z;
self.valid = true;
}
#[must_use]
pub fn magnitude(&self) -> f32 {
(self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
}
#[must_use]
pub fn is_shaking(&self, threshold: f32) -> bool {
let accel_without_gravity = self.magnitude() - 9.8;
accel_without_gravity.abs() > threshold
}
#[cfg(feature = "motion-backends")]
pub fn update_from_backend(&mut self, data: &crate::motion::backend::MotionData) {
self.set_raw(data.accel_x, data.accel_y, data.accel_z);
}
}
#[derive(Debug, Clone, Resource)]
pub struct MotionConfig {
pub gyro_sensitivity: f32,
pub gyro_deadzone: f32,
pub accel_sensitivity: f32,
pub enabled: bool,
}
impl Default for MotionConfig {
fn default() -> Self {
Self {
gyro_sensitivity: 1.0,
gyro_deadzone: 0.01,
accel_sensitivity: 1.0,
enabled: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MotionGesture {
Flick,
Tilt,
Shake,
Roll,
}
#[derive(Debug, Clone, Message)]
pub struct MotionGestureDetected {
pub gamepad: Entity,
pub gesture: MotionGesture,
pub intensity: f32,
}
pub fn update_gyro_data(
mut gamepads: Query<(Entity, &Gamepad, Option<&mut GyroData>)>,
mut commands: Commands,
) {
for (entity, _gamepad, gyro) in &mut gamepads {
if gyro.is_none() {
commands.entity(entity).insert(GyroData::default());
}
}
}
pub fn update_accel_data(
mut gamepads: Query<(Entity, &Gamepad, Option<&mut AccelData>)>,
mut commands: Commands,
) {
for (entity, _gamepad, accel) in &mut gamepads {
if accel.is_none() {
commands.entity(entity).insert(AccelData::default());
}
}
}
pub fn detect_motion_gestures(
gamepads: Query<(Entity, &GyroData, &AccelData)>,
config: Res<MotionConfig>,
mut gesture_events: MessageWriter<MotionGestureDetected>,
) {
if !config.enabled {
return;
}
for (entity, gyro, accel) in gamepads.iter() {
if !gyro.valid || !accel.valid {
continue;
}
if gyro.magnitude() > 5.0 {
gesture_events.write(MotionGestureDetected {
gamepad: entity,
gesture: MotionGesture::Flick,
intensity: gyro.magnitude(),
});
}
if accel.is_shaking(3.0) {
gesture_events.write(MotionGestureDetected {
gamepad: entity,
gesture: MotionGesture::Shake,
intensity: accel.magnitude(),
});
}
}
}
pub(crate) fn register_gyro_types(app: &mut App) {
app.register_type::<GyroData>()
.register_type::<AccelData>()
.init_resource::<MotionConfig>()
.add_message::<MotionGestureDetected>();
}
pub(crate) fn add_gyro_systems(app: &mut App) {
app.add_systems(
Update,
(update_gyro_data, update_accel_data, detect_motion_gestures).chain(),
);
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_gyro_data_new() {
let gyro = GyroData::new(1.0, 2.0, 3.0);
assert_relative_eq!(gyro.pitch, 1.0);
assert_relative_eq!(gyro.yaw, 2.0);
assert_relative_eq!(gyro.roll, 3.0);
assert!(gyro.valid);
}
#[test]
fn test_gyro_data_magnitude() {
let gyro = GyroData::new(3.0, 4.0, 0.0);
assert_relative_eq!(gyro.magnitude(), 5.0);
}
#[test]
fn test_gyro_data_exceeds_threshold() {
let gyro = GyroData::new(3.0, 4.0, 0.0);
assert!(gyro.exceeds_threshold(4.0));
assert!(!gyro.exceeds_threshold(6.0));
}
#[test]
fn test_gyro_data_default() {
let gyro = GyroData::default();
assert_relative_eq!(gyro.pitch, 0.0);
assert_relative_eq!(gyro.yaw, 0.0);
assert_relative_eq!(gyro.roll, 0.0);
assert!(!gyro.valid);
}
#[test]
fn test_accel_data_new() {
let accel = AccelData::new(1.0, 2.0, 3.0);
assert_relative_eq!(accel.x, 1.0);
assert_relative_eq!(accel.y, 2.0);
assert_relative_eq!(accel.z, 3.0);
assert!(accel.valid);
}
#[test]
fn test_accel_data_magnitude() {
let accel = AccelData::new(3.0, 4.0, 0.0);
assert_relative_eq!(accel.magnitude(), 5.0);
}
#[test]
fn test_accel_data_is_shaking() {
let accel = AccelData::new(0.0, 20.0, 0.0); assert!(accel.is_shaking(5.0));
let still = AccelData::new(0.0, 9.8, 0.0); assert!(!still.is_shaking(5.0));
}
#[test]
fn test_accel_data_default() {
let accel = AccelData::default();
assert_relative_eq!(accel.x, 0.0);
assert_relative_eq!(accel.y, 0.0);
assert_relative_eq!(accel.z, 0.0);
assert!(!accel.valid);
}
#[test]
fn test_motion_gesture_variants() {
assert_ne!(MotionGesture::Shake, MotionGesture::Tilt);
assert_ne!(MotionGesture::Roll, MotionGesture::Flick);
}
#[test]
fn test_motion_config_default() {
let config = MotionConfig::default();
assert!(config.gyro_sensitivity > 0.0);
assert!(config.accel_sensitivity > 0.0);
assert!(config.gyro_deadzone > 0.0);
assert!(config.enabled);
}
#[test]
fn test_motion_gesture_detected_event() {
let gamepad = Entity::from_bits(77);
let event = MotionGestureDetected {
gamepad,
gesture: MotionGesture::Shake,
intensity: 0.8,
};
assert_eq!(event.gamepad, gamepad);
assert_eq!(event.gesture, MotionGesture::Shake);
assert_relative_eq!(event.intensity, 0.8);
}
#[test]
fn test_gyro_data_zero_magnitude() {
let gyro = GyroData::new(0.0, 0.0, 0.0);
assert_relative_eq!(gyro.magnitude(), 0.0);
}
#[test]
fn test_gyro_data_invalid() {
let mut gyro = GyroData::new(1.0, 2.0, 3.0);
gyro.valid = false;
assert!(!gyro.valid);
}
#[test]
fn test_accel_data_zero_magnitude() {
let accel = AccelData::new(0.0, 0.0, 0.0);
assert_relative_eq!(accel.magnitude(), 0.0);
}
#[test]
fn test_accel_data_invalid() {
let mut accel = AccelData::new(1.0, 2.0, 3.0);
accel.valid = false;
assert!(!accel.valid);
}
#[test]
fn test_motion_gesture_all_variants() {
let gestures = [
MotionGesture::Flick,
MotionGesture::Tilt,
MotionGesture::Shake,
MotionGesture::Roll,
];
for (i, &g1) in gestures.iter().enumerate() {
for (j, &g2) in gestures.iter().enumerate() {
if i != j {
assert_ne!(g1, g2);
}
}
}
}
#[test]
fn test_motion_config_custom_values() {
let config = MotionConfig {
gyro_sensitivity: 2.0,
gyro_deadzone: 0.05,
accel_sensitivity: 1.5,
enabled: false,
};
assert_relative_eq!(config.gyro_sensitivity, 2.0);
assert_relative_eq!(config.gyro_deadzone, 0.05);
assert_relative_eq!(config.accel_sensitivity, 1.5);
assert!(!config.enabled);
}
#[test]
fn test_gyro_data_different_thresholds() {
let gyro = GyroData::new(5.0, 0.0, 0.0);
assert_relative_eq!(gyro.magnitude(), 5.0);
assert!(gyro.exceeds_threshold(4.0));
assert!(!gyro.exceeds_threshold(5.0)); let accel = AccelData::new(0.0, 9.8, 0.0);
assert_relative_eq!(accel.magnitude(), 9.8, epsilon = 0.1);
assert!(!accel.is_shaking(1.0));
}
#[test]
fn test_accel_data_strong_shake() {
let accel = AccelData::new(10.0, 15.0, 5.0);
assert!(accel.is_shaking(5.0));
}
#[test]
fn test_motion_gesture_detected_different_gestures() {
let gamepad = Entity::from_bits(1);
let flick = MotionGestureDetected {
gamepad,
gesture: MotionGesture::Flick,
intensity: 1.0,
};
let shake = MotionGestureDetected {
gamepad,
gesture: MotionGesture::Shake,
intensity: 0.5,
};
assert_ne!(flick.gesture, shake.gesture);
}
}