synvael/crates/client/src/debug.rs

92 lines
4.2 KiB
Rust

// 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**: filled terrain with vertex points overlaid, showing where the mesher placed geometry without losing the surface.
//! - **F1 + B**: filled terrain with the triangle edges overlaid, showing the size and shape of the emitted quads.
//!
//! Holding a [`SOLO_MODIFIER`] (either Shift) as well drops the filled pass, leaving the debug geometry alone against the clear colour: **F1 + Shift + V** for points only, **F1 + Shift + B** for wireframe only.
//!
//! Each chord toggles: pressing the chord for the active mode 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;
/// The keys that, held alongside [`DEBUG_MODIFIER`], select the solo form of a debug view. Both shifts are accepted so the chord is reachable with either hand.
const SOLO_MODIFIER: [KeyCode; 2] = [KeyCode::ShiftLeft, KeyCode::ShiftRight];
/// 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,
/// Whether a [`SOLO_MODIFIER`] is currently held, selecting the solo form of the chord.
solo_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;
}
// A solo modifier is tracked unconditionally rather than only while the debug modifier is held, so its state is correct whichever of the two is pressed first.
if SOLO_MODIFIER.contains(&code) {
self.solo_held = pressed;
return None;
}
if !pressed || !self.modifier_held {
return None;
}
let requested = render_mode_for_key(code, self.solo_held)?;
// 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, and whether a [`SOLO_MODIFIER`] is held, to the render mode it selects. Returns [`None`] if the key is unbound.
///
/// This is the single table a new rasterisation debug mode is added to: one key, one overlaid form, one solo form.
const fn render_mode_for_key(code: KeyCode, solo: bool) -> Option<RenderMode> {
match (code, solo) {
(KeyCode::KeyV, false) => Some(RenderMode::FilledPoints),
(KeyCode::KeyV, true) => Some(RenderMode::Points),
(KeyCode::KeyB, false) => Some(RenderMode::FilledWireframe),
(KeyCode::KeyB, true) => Some(RenderMode::Wireframe),
_ => None,
}
}
#[cfg(test)]
#[path = "tests/debug.rs"]
mod tests;