feat(client): add debug controls for toggling render modes
This commit is contained in:
parent
5f4b30a4dc
commit
5c10aefee9
72
crates/client/src/debug.rs
Normal file
72
crates/client/src/debug.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
//! Debug-only input handling, kept separate from the gameplay input path.
|
||||
//!
|
||||
//! Debug affordances are bound behind a modifier chord so they cannot collide with movement keys: [`DEBUG_MODIFIER`] (F1) is held, and a second key selects the affordance. The currently bound chords are:
|
||||
//!
|
||||
//! - **F1 + V**: toggles the [`RenderMode::Points`] debug rasterisation mode, which draws one point per mesh vertex. Pressing it again returns to [`RenderMode::Filled`].
|
||||
|
||||
use renderer::RenderMode;
|
||||
use winit::keyboard::KeyCode;
|
||||
|
||||
/// The key that must be held for a debug chord to be recognised.
|
||||
const DEBUG_MODIFIER: KeyCode = KeyCode::F1;
|
||||
|
||||
/// A debug operation requested by the input layer, applied by the caller.
|
||||
///
|
||||
/// The layer deliberately returns an intent rather than acting directly, so it owns no renderer or window handles and stays a pure function of key events.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum DebugAction {
|
||||
/// Applies the given rasterisation mode to the renderer.
|
||||
SetRenderMode(RenderMode),
|
||||
}
|
||||
|
||||
/// Owns debug-only input state and translates key events into [`DebugAction`]s.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct DebugControls {
|
||||
/// Whether [`DEBUG_MODIFIER`] is currently held. Chords are recognised only while this is set.
|
||||
modifier_held: bool,
|
||||
/// The rasterisation mode most recently requested, used to make each chord a toggle back to [`RenderMode::Filled`].
|
||||
render_mode: RenderMode,
|
||||
}
|
||||
|
||||
impl DebugControls {
|
||||
/// Translates one key event into a debug action, updating internal state.
|
||||
///
|
||||
/// Returns [`None`] when the event is not part of a debug chord, which is the common case; the caller then handles the key normally. Actions fire on the press edge only, so one physical tap toggles once rather than once per press and once per release.
|
||||
pub(crate) fn handle_key(&mut self, code: KeyCode, pressed: bool) -> Option<DebugAction> {
|
||||
if code == DEBUG_MODIFIER {
|
||||
self.modifier_held = pressed;
|
||||
return None;
|
||||
}
|
||||
|
||||
if !pressed || !self.modifier_held {
|
||||
return None;
|
||||
}
|
||||
|
||||
let requested = render_mode_for_key(code)?;
|
||||
|
||||
// Re-pressing the chord for the active mode returns to the normal path, so a single chord both enables and disables its mode.
|
||||
self.render_mode = if self.render_mode == requested {
|
||||
RenderMode::Filled
|
||||
} else {
|
||||
requested
|
||||
};
|
||||
|
||||
Some(DebugAction::SetRenderMode(self.render_mode))
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a chord key to the render mode it selects, or [`None`] if the key is unbound.
|
||||
///
|
||||
/// This is the single table a new rasterisation debug mode is added to.
|
||||
const fn render_mode_for_key(code: KeyCode) -> Option<RenderMode> {
|
||||
match code {
|
||||
KeyCode::KeyV => Some(RenderMode::Points),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/debug.rs"]
|
||||
mod tests;
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
mod camera;
|
||||
mod chunks;
|
||||
mod debug;
|
||||
mod mesh_pool;
|
||||
|
||||
use std::time::Instant;
|
||||
|
|
@ -56,6 +57,8 @@ struct App {
|
|||
camera: Camera,
|
||||
/// The current keyboard and mouse input state.
|
||||
input: InputState,
|
||||
/// Debug-only key handling, kept separate from the gameplay input path.
|
||||
debug: debug::DebugControls,
|
||||
/// Timestamp of the previous frame, used to derive delta-time. `None` before the first frame.
|
||||
last_frame: Option<Instant>,
|
||||
/// Handles onto the background network connection: the handshake outcome, the chunk-subscription sender, and the chunk-delivery receiver. `None` before the connection is started.
|
||||
|
|
@ -80,6 +83,7 @@ impl Default for App {
|
|||
-0.5,
|
||||
),
|
||||
input: InputState::default(),
|
||||
debug: debug::DebugControls::default(),
|
||||
last_frame: None,
|
||||
link: None,
|
||||
connected: false,
|
||||
|
|
@ -89,6 +93,22 @@ impl Default for App {
|
|||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Applies a debug action produced by [`debug::DebugControls`].
|
||||
///
|
||||
/// Actions targeting the renderer are dropped while it is uninitialised, which is the window between application start and the first `resumed` call.
|
||||
fn apply_debug_action(&mut self, action: debug::DebugAction) {
|
||||
match action {
|
||||
debug::DebugAction::SetRenderMode(mode) => {
|
||||
if let Some(renderer) = self.renderer.as_mut() {
|
||||
renderer.set_render_mode(mode);
|
||||
info!(?mode, "render mode toggled");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let attributes = Window::default_attributes().with_title("Synvael");
|
||||
|
|
@ -194,6 +214,11 @@ impl ApplicationHandler for App {
|
|||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
let pressed = event.state == ElementState::Pressed;
|
||||
if let PhysicalKey::Code(code) = event.physical_key {
|
||||
// Debug chords are resolved first and on their own seam, so debug bindings can grow without entangling the gameplay bindings below.
|
||||
if let Some(action) = self.debug.handle_key(code, pressed) {
|
||||
self.apply_debug_action(action);
|
||||
}
|
||||
|
||||
match code {
|
||||
KeyCode::KeyW => self.input.forward = pressed,
|
||||
KeyCode::KeyS => self.input.backward = pressed,
|
||||
|
|
|
|||
69
crates/client/src/tests/debug.rs
Normal file
69
crates/client/src/tests/debug.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
//! Unit tests for the debug chord handling in [`crate::debug`].
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Presses and releases a key, returning the action produced on the press edge.
|
||||
fn tap(controls: &mut DebugControls, code: KeyCode) -> Option<DebugAction> {
|
||||
let action = controls.handle_key(code, true);
|
||||
controls.handle_key(code, false);
|
||||
action
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chord_key_alone_does_nothing() {
|
||||
let mut controls = DebugControls::default();
|
||||
assert_eq!(tap(&mut controls, KeyCode::KeyV), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_alone_produces_no_action() {
|
||||
let mut controls = DebugControls::default();
|
||||
assert_eq!(controls.handle_key(DEBUG_MODIFIER, true), None);
|
||||
assert_eq!(controls.handle_key(DEBUG_MODIFIER, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn held_modifier_plus_bound_key_selects_the_mode() {
|
||||
let mut controls = DebugControls::default();
|
||||
controls.handle_key(DEBUG_MODIFIER, true);
|
||||
assert_eq!(
|
||||
tap(&mut controls, KeyCode::KeyV),
|
||||
Some(DebugAction::SetRenderMode(RenderMode::Points))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeating_the_chord_toggles_back_to_filled() {
|
||||
let mut controls = DebugControls::default();
|
||||
controls.handle_key(DEBUG_MODIFIER, true);
|
||||
tap(&mut controls, KeyCode::KeyV);
|
||||
assert_eq!(
|
||||
tap(&mut controls, KeyCode::KeyV),
|
||||
Some(DebugAction::SetRenderMode(RenderMode::Filled))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_fires_on_the_press_edge_only() {
|
||||
let mut controls = DebugControls::default();
|
||||
controls.handle_key(DEBUG_MODIFIER, true);
|
||||
assert!(controls.handle_key(KeyCode::KeyV, true).is_some());
|
||||
assert_eq!(controls.handle_key(KeyCode::KeyV, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn releasing_the_modifier_disarms_the_chord() {
|
||||
let mut controls = DebugControls::default();
|
||||
controls.handle_key(DEBUG_MODIFIER, true);
|
||||
controls.handle_key(DEBUG_MODIFIER, false);
|
||||
assert_eq!(tap(&mut controls, KeyCode::KeyV), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbound_key_under_the_modifier_is_ignored() {
|
||||
let mut controls = DebugControls::default();
|
||||
controls.handle_key(DEBUG_MODIFIER, true);
|
||||
assert_eq!(tap(&mut controls, KeyCode::KeyW), None);
|
||||
}
|
||||
Loading…
Reference in a new issue