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 camera;
|
||||||
mod chunks;
|
mod chunks;
|
||||||
|
mod debug;
|
||||||
mod mesh_pool;
|
mod mesh_pool;
|
||||||
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
@ -56,6 +57,8 @@ struct App {
|
||||||
camera: Camera,
|
camera: Camera,
|
||||||
/// The current keyboard and mouse input state.
|
/// The current keyboard and mouse input state.
|
||||||
input: InputState,
|
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.
|
/// Timestamp of the previous frame, used to derive delta-time. `None` before the first frame.
|
||||||
last_frame: Option<Instant>,
|
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.
|
/// 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,
|
-0.5,
|
||||||
),
|
),
|
||||||
input: InputState::default(),
|
input: InputState::default(),
|
||||||
|
debug: debug::DebugControls::default(),
|
||||||
last_frame: None,
|
last_frame: None,
|
||||||
link: None,
|
link: None,
|
||||||
connected: false,
|
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 {
|
impl ApplicationHandler for App {
|
||||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
let attributes = Window::default_attributes().with_title("Synvael");
|
let attributes = Window::default_attributes().with_title("Synvael");
|
||||||
|
|
@ -194,6 +214,11 @@ impl ApplicationHandler for App {
|
||||||
WindowEvent::KeyboardInput { event, .. } => {
|
WindowEvent::KeyboardInput { event, .. } => {
|
||||||
let pressed = event.state == ElementState::Pressed;
|
let pressed = event.state == ElementState::Pressed;
|
||||||
if let PhysicalKey::Code(code) = event.physical_key {
|
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 {
|
match code {
|
||||||
KeyCode::KeyW => self.input.forward = pressed,
|
KeyCode::KeyW => self.input.forward = pressed,
|
||||||
KeyCode::KeyS => self.input.backward = 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