diff --git a/src/main.rs b/src/main.rs index 3de7531..e781d22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,70 +1,32 @@ +use crate::shape_follow::ShapeFollowPlugin; +use crate::utils::get_cursor_world_pos; use bevy::color::palettes::basic::{RED, WHITE}; use bevy::prelude::*; mod fps_counter; +mod shape_follow; +mod utils; fn main() { App::new() .add_plugins(( DefaultPlugins, fps_counter::FpsCounterPlugin, - MeshPickingPlugin, + ShapeFollowPlugin, )) .add_systems(Startup, (setup_camera, change_window_mode)) - .add_systems(Startup, setup_shapes) - .add_systems(Update, show_focused_shape) - .add_systems(FixedUpdate, focused_follow_cursor) .add_systems(Update, draw_cursor) .run(); } -const CURSOR_SIZE: f32 = 10.0; -const FOCUS_MARK_SIZE: f32 = 10.0; -const SHAPE_Z_POS: f32 = 0.0; -const SHAPE_FOLLOW_SPEED: f32 = 100.0; +const CURSOR_SIZE: f32 = 10.0; fn change_window_mode(mut windows: Query<&mut Window>) { let mut window = windows.single_mut(); // window.mode = WindowMode::Fullscreen(MonitorSelection::Current); window.present_mode = bevy::window::PresentMode::AutoNoVsync; } - fn setup_camera(mut commands: Commands) { commands.spawn(Camera2d); } - -fn setup_shapes( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - let shapes = [ - meshes.add(Rectangle::new(50.0, 100.0)), - meshes.add(Rectangle::new(100.0, 50.0)), - ]; - let num_shapes = shapes.len(); - for (i, shape) in shapes.into_iter().enumerate() { - commands - .spawn(( - Mesh2d(shape), - MeshMaterial2d(materials.add(Color::from(RED))), - Focused(true), - Transform::from_xyz( - (i as f32 - num_shapes as f32 / 2.0) * 150.0, - 0.0, - SHAPE_Z_POS, - ), - )) - .observe(on_click_shape); - } -} - -fn on_click_shape(click: Trigger>, mut focused: Query<&mut Focused>) { - let Ok(mut focused) = focused.get_mut(click.entity()) else { - return; - }; - - focused.0 = !focused.0; -} - fn draw_cursor( camera_query: Single<(&Camera, &GlobalTransform)>, windows: Query<&Window>, @@ -76,60 +38,3 @@ fn draw_cursor( gizmos.circle_2d(point, CURSOR_SIZE, WHITE); } - -fn get_cursor_world_pos( - camera_query: Single<(&Camera, &GlobalTransform)>, - windows: Query<&Window>, -) -> Option { - let (camera, camera_transform) = *camera_query; - let cursor_position = windows.get_single().ok()?.cursor_position()?; - - let point = camera - .viewport_to_world_2d(camera_transform, cursor_position) - .ok()?; - Some(point) -} - -#[derive(Component)] -struct Focused(bool); - -fn show_focused_shape(focused: Query<(&Focused, &GlobalTransform)>, mut gizmos: Gizmos) { - for (focused, transform) in focused.iter() { - let point: Vec2 = transform.translation().xy(); - if focused.0 { - gizmos.circle_2d(point, FOCUS_MARK_SIZE, WHITE); - } - } -} -fn focused_follow_cursor( - time: Res>, - mut focused: Query<(&Focused, &mut Transform)>, - camera_query: Single<(&Camera, &GlobalTransform)>, - windows: Query<&Window>, -) { - let Some(point) = get_cursor_world_pos(camera_query, windows) else { - return; - }; - for (focused, mut transform) in focused.iter_mut() { - if focused.0 { - move_towards( - point, - &mut transform, - time.delta_secs() * SHAPE_FOLLOW_SPEED, - ); - } - } -} - -fn move_towards(point: Vec2, transform: &mut Mut, speed_per_frame: f32) { - let move_vec = point - transform.translation.xy(); - if move_vec.length_squared() < 0.0001 { - return; - } - let capped_move_vec = move_vec.normalize() * speed_per_frame; - if capped_move_vec.length_squared() > move_vec.length_squared() { - // prevent overshooting if we are already close to the target - transform.translation += move_vec.extend(SHAPE_Z_POS); - } - transform.translation += capped_move_vec.extend(SHAPE_Z_POS); -} diff --git a/src/shape_follow.rs b/src/shape_follow.rs new file mode 100644 index 0000000..7617555 --- /dev/null +++ b/src/shape_follow.rs @@ -0,0 +1,95 @@ +use crate::utils::Vec2CapToVec2; +use crate::utils::get_cursor_world_pos; +use bevy::asset::Assets; +use bevy::color::Color; +use bevy::color::palettes::basic::{RED, WHITE}; +use bevy::math::Vec2; +use bevy::prelude::*; + +const FOCUS_MARK_SIZE: f32 = 10.0; +const SHAPE_Z_POS: f32 = 0.0; +const SHAPE_FOLLOW_SPEED: f32 = 100.0; +pub struct ShapeFollowPlugin; +impl Plugin for ShapeFollowPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(MeshPickingPlugin) + .add_systems(Startup, setup_shapes) + .add_systems(Update, show_focused_shape) + .add_systems(FixedUpdate, focused_follow_cursor); + } +} +fn setup_shapes( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let shapes = [ + meshes.add(Rectangle::new(50.0, 100.0)), + meshes.add(Rectangle::new(100.0, 50.0)), + ]; + let num_shapes = shapes.len(); + for (i, shape) in shapes.into_iter().enumerate() { + commands + .spawn(( + Mesh2d(shape), + MeshMaterial2d(materials.add(Color::from(RED))), + Focused(true), + Transform::from_xyz( + (i as f32 - num_shapes as f32 / 2.0) * 150.0, + 0.0, + SHAPE_Z_POS, + ), + )) + .observe(on_click_shape); + } +} + +fn on_click_shape(click: Trigger>, mut focused: Query<&mut Focused>) { + let Ok(mut focused) = focused.get_mut(click.entity()) else { + return; + }; + + focused.0 = !focused.0; +} + +#[derive(Component)] +struct Focused(bool); + +fn show_focused_shape(focused: Query<(&Focused, &GlobalTransform)>, mut gizmos: Gizmos) { + for (focused, transform) in focused.iter() { + let point: Vec2 = transform.translation().xy(); + if focused.0 { + gizmos.circle_2d(point, FOCUS_MARK_SIZE, WHITE); + } + } +} +fn focused_follow_cursor( + time: Res>, + mut focused: Query<(&Focused, &mut Transform)>, + camera_query: Single<(&Camera, &GlobalTransform)>, + windows: Query<&Window>, +) { + let Some(point) = get_cursor_world_pos(camera_query, windows) else { + return; + }; + for (focused, mut transform) in focused.iter_mut() { + if focused.0 { + move_towards( + point, + &mut transform, + time.delta_secs() * SHAPE_FOLLOW_SPEED, + ); + } + } +} + +fn move_towards(point: Vec2, transform: &mut Mut, speed_per_frame: f32) { + let move_vec = point - transform.translation.xy(); + if move_vec.length_squared() < 0.0001 { + return; + } + let capped_move_vec = move_vec.normalize() * speed_per_frame; + // prevent overshooting if we are already close to the target + let capped_move_vec = capped_move_vec.cap_to_vec2(move_vec); + transform.translation += capped_move_vec.extend(SHAPE_Z_POS); +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..abf7577 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,33 @@ +use bevy::math::Vec2; +use bevy::prelude::{Camera, GlobalTransform, Query, Single, Window}; + +pub fn get_cursor_world_pos( + camera_query: Single<(&Camera, &GlobalTransform)>, + windows: Query<&Window>, +) -> Option { + let (camera, camera_transform) = *camera_query; + let cursor_position = windows.get_single().ok()?.cursor_position()?; + + let point = camera + .viewport_to_world_2d(camera_transform, cursor_position) + .ok()?; + Some(point) +} + +pub trait Vec2CapToVec2 { + fn cap_to_vec2(self, max: Vec2) -> Vec2; +} +impl Vec2CapToVec2 for Vec2 { + fn cap_to_vec2(self, max: Vec2) -> Vec2 { + let mut capped_move_vec = self; + if (self.x > 0.0 && self.x > max.x) || (self.x < 0.0 && self.x < max.x) { + capped_move_vec.x = max.x; + } + if (self.y > 0.0 && capped_move_vec.y > max.y) + || (self.y < 0.0 && capped_move_vec.y < max.y) + { + capped_move_vec.y = max.y; + } + capped_move_vec + } +}