From 90adaf9e84ad17eb033c056964e5d1b6c86313ff Mon Sep 17 00:00:00 2001 From: Xevion Date: Sat, 16 Aug 2025 12:26:24 -0500 Subject: [PATCH] feat: add cursor-based node highlighting for debug --- src/game/mod.rs | 3 +- src/systems/debug.rs | 78 +++++++++++++++++++++++++++++++++++------ src/systems/input.rs | 6 ++++ src/systems/movement.rs | 13 ------- 4 files changed, 75 insertions(+), 25 deletions(-) diff --git a/src/game/mod.rs b/src/game/mod.rs index 2d95212..0ac0568 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -47,7 +47,7 @@ use crate::{ constants, events::GameCommand, map::render::MapRenderer, - systems::input::Bindings, + systems::{debug::CursorPosition, input::Bindings}, texture::sprite::{AtlasMapper, SpriteAtlas}, }; @@ -198,6 +198,7 @@ impl Game { world.insert_resource(RenderDirty::default()); world.insert_resource(DebugState::default()); world.insert_resource(AudioState::default()); + world.insert_resource(CursorPosition::default()); world.add_observer( |event: Trigger, mut state: ResMut, _score: ResMut| { diff --git a/src/systems/debug.rs b/src/systems/debug.rs index f106a23..6897679 100644 --- a/src/systems/debug.rs +++ b/src/systems/debug.rs @@ -1,4 +1,6 @@ //! Debug rendering system +use std::cmp::Ordering; + use crate::constants::BOARD_PIXEL_OFFSET; use crate::map::builder::Map; use crate::systems::components::Collider; @@ -6,11 +8,15 @@ use crate::systems::movement::Position; use crate::systems::profiling::SystemTimings; use crate::systems::render::BackbufferResource; use bevy_ecs::prelude::*; +use glam::Vec2; use sdl2::pixels::Color; use sdl2::rect::Rect; use sdl2::render::{Canvas, Texture, TextureCreator}; use sdl2::video::{Window, WindowContext}; +#[derive(Resource, Default, Debug, Copy, Clone)] +pub struct CursorPosition(pub Vec2); + #[derive(Resource, Default, Debug, Copy, Clone, PartialEq)] pub enum DebugState { #[default] @@ -36,10 +42,13 @@ pub struct DebugTextureResource(pub Texture<'static>); fn transform_position(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) { let scale_x = output_size.0 as f32 / logical_size.0 as f32; let scale_y = output_size.1 as f32 / logical_size.1 as f32; - let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio + let scale = scale_x.min(scale_y); - let x = (pos.0 * scale) as i32; - let y = (pos.1 * scale) as i32; + let offset_x = (output_size.0 as f32 - logical_size.0 as f32 * scale) / 2.0; + let offset_y = (output_size.1 as f32 - logical_size.1 as f32 * scale) / 2.0; + + let x = (pos.0 * scale + offset_x) as i32; + let y = (pos.1 * scale + offset_y) as i32; (x, y) } @@ -47,10 +56,13 @@ fn transform_position(pos: (f32, f32), output_size: (u32, u32), logical_size: (u fn transform_position_with_offset(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) { let scale_x = output_size.0 as f32 / logical_size.0 as f32; let scale_y = output_size.1 as f32 / logical_size.1 as f32; - let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio + let scale = scale_x.min(scale_y); - let x = ((pos.0 + BOARD_PIXEL_OFFSET.x as f32) * scale) as i32; - let y = ((pos.1 + BOARD_PIXEL_OFFSET.y as f32) * scale) as i32; + let offset_x = (output_size.0 as f32 - logical_size.0 as f32 * scale) / 2.0; + let offset_y = (output_size.1 as f32 - logical_size.1 as f32 * scale) / 2.0; + + let x = ((pos.0 + BOARD_PIXEL_OFFSET.x as f32) * scale + offset_x) as i32; + let y = ((pos.1 + BOARD_PIXEL_OFFSET.y as f32) * scale + offset_y) as i32; (x, y) } @@ -129,6 +141,7 @@ pub fn debug_render_system( timings: Res, map: Res, colliders: Query<(&Collider, &Position)>, + cursor: Res, ) { if *debug_state == DebugState::Off { return; @@ -153,28 +166,49 @@ pub fn debug_render_system( // Get texture creator before entering the closure to avoid borrowing conflicts let mut texture_creator = canvas.texture_creator(); + let cursor_world_pos = cursor.0 - BOARD_PIXEL_OFFSET.as_vec2(); + // Draw debug info on the high-resolution debug texture canvas .with_texture_canvas(&mut debug_texture.0, |debug_canvas| { match *debug_state { DebugState::Graph => { + // Find the closest node to the cursor + + let closest_node = map + .graph + .nodes() + .map(|node| node.position.distance(cursor_world_pos)) + .enumerate() + .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Less)) + .map(|(id, _)| id); + debug_canvas.set_draw_color(Color::RED); for (start_node, end_node) in map.graph.edges() { - let start_node = map.graph.get_node(start_node).unwrap().position; + let start_node_model = map.graph.get_node(start_node).unwrap(); let end_node = map.graph.get_node(end_node.target).unwrap().position; // Transform positions using common method - let (start_x, start_y) = - transform_position_with_offset((start_node.x, start_node.y), output_size, logical_size); + let (start_x, start_y) = transform_position_with_offset( + (start_node_model.position.x, start_node_model.position.y), + output_size, + logical_size, + ); let (end_x, end_y) = transform_position_with_offset((end_node.x, end_node.y), output_size, logical_size); debug_canvas.draw_line((start_x, start_y), (end_x, end_y)).unwrap(); } - debug_canvas.set_draw_color(Color::BLUE); - for node in map.graph.nodes() { + for (id, node) in map.graph.nodes().enumerate() { let pos = node.position; + // Set color based on whether the node is the closest to the cursor + if Some(id) == closest_node { + debug_canvas.set_draw_color(Color::YELLOW); + } else { + debug_canvas.set_draw_color(Color::BLUE); + } + // Transform position using common method let (x, y) = transform_position_with_offset((pos.x, pos.y), output_size, logical_size); let size = transform_size(4.0, output_size, logical_size); @@ -201,6 +235,28 @@ pub fn debug_render_system( _ => {} } + // Render node ID if a node is highlighted + if let DebugState::Graph = *debug_state { + if let Some(closest_node_id) = map + .graph + .nodes() + .map(|node| node.position.distance(cursor_world_pos)) + .enumerate() + .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Less)) + .map(|(id, _)| id) + { + let node = map.graph.get_node(closest_node_id).unwrap(); + let (x, y) = transform_position_with_offset((node.position.x, node.position.y), output_size, logical_size); + + let ttf_context = sdl2::ttf::init().unwrap(); + let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap(); + let surface = font.render(&closest_node_id.to_string()).blended(Color::WHITE).unwrap(); + let texture = texture_creator.create_texture_from_surface(&surface).unwrap(); + let dest = Rect::new(x + 10, y - 5, texture.query().width, texture.query().height); + debug_canvas.copy(&texture, None, dest).unwrap(); + } + } + // Render timing information in the top-left corner render_timing_display(debug_canvas, &mut texture_creator, &timings); }) diff --git a/src/systems/input.rs b/src/systems/input.rs index 0620539..44d4762 100644 --- a/src/systems/input.rs +++ b/src/systems/input.rs @@ -5,8 +5,10 @@ use bevy_ecs::{ resource::Resource, system::{NonSendMut, ResMut}, }; +use glam::Vec2; use sdl2::{event::Event, keyboard::Keycode, EventPump}; +use crate::systems::debug::CursorPosition; use crate::{ entity::direction::Direction, events::{GameCommand, GameEvent}, @@ -64,6 +66,7 @@ pub fn input_system( mut bindings: ResMut, mut writer: EventWriter, mut pump: NonSendMut<&'static mut EventPump>, + mut cursor: ResMut, ) { let mut movement_key_pressed = false; @@ -72,6 +75,9 @@ pub fn input_system( Event::Quit { .. } => { writer.write(GameEvent::Command(GameCommand::Exit)); } + Event::MouseMotion { x, y, .. } => { + cursor.0 = Vec2::new(x as f32, y as f32); + } Event::KeyUp { repeat: false, keycode: Some(key), diff --git a/src/systems/movement.rs b/src/systems/movement.rs index 0b9c522..60195b4 100644 --- a/src/systems/movement.rs +++ b/src/systems/movement.rs @@ -126,19 +126,6 @@ impl Position { Position::Moving { from, .. } => *from, } } - - /// Returns the `NodeId` of the destination node, if currently traveling. - pub fn target_node(&self) -> Option { - match self { - Position::Stopped { .. } => None, - Position::Moving { to, .. } => Some(*to), - } - } - - /// Returns `true` if the entity is traveling between nodes. - pub fn is_moving(&self) -> bool { - matches!(self, Position::Moving { .. }) - } } // pub fn movement_system(