feat(client): add a solo modifier for debug view chords

This commit is contained in:
Serkyo 2026-07-27 21:36:14 +02:00
parent 324d1b144d
commit 1d041e035e
2 changed files with 100 additions and 7 deletions

View file

@ -4,7 +4,12 @@
//!
//! 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`].
//! - **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;
@ -12,6 +17,9 @@ 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.
@ -26,6 +34,8 @@ pub(crate) enum DebugAction {
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,
}
@ -40,11 +50,17 @@ impl DebugControls {
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)?;
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 {
@ -57,12 +73,15 @@ impl DebugControls {
}
}
/// Maps a chord key to the render mode it selects, or [`None`] if the key is unbound.
/// 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.
const fn render_mode_for_key(code: KeyCode) -> Option<RenderMode> {
match code {
KeyCode::KeyV => Some(RenderMode::Points),
/// 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,
}
}

View file

@ -28,12 +28,79 @@ fn modifier_alone_produces_no_action() {
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::FilledPoints))
);
}
#[test]
fn each_bound_key_selects_a_distinct_overlay_mode() {
let mut controls = DebugControls::default();
controls.handle_key(DEBUG_MODIFIER, true);
for (code, expected) in [
(KeyCode::KeyV, RenderMode::FilledPoints),
(KeyCode::KeyB, RenderMode::FilledWireframe),
] {
assert_eq!(
tap(&mut controls, code),
Some(DebugAction::SetRenderMode(expected))
);
}
}
#[test]
fn solo_modifier_drops_the_filled_pass() {
for solo in SOLO_MODIFIER {
let mut controls = DebugControls::default();
controls.handle_key(DEBUG_MODIFIER, true);
controls.handle_key(solo, true);
assert_eq!(
tap(&mut controls, KeyCode::KeyV),
Some(DebugAction::SetRenderMode(RenderMode::Points))
);
assert_eq!(
tap(&mut controls, KeyCode::KeyB),
Some(DebugAction::SetRenderMode(RenderMode::Wireframe))
);
}
}
#[test]
fn solo_modifier_is_tracked_before_the_debug_modifier() {
let mut controls = DebugControls::default();
controls.handle_key(KeyCode::ShiftLeft, true);
controls.handle_key(DEBUG_MODIFIER, true);
assert_eq!(
tap(&mut controls, KeyCode::KeyV),
Some(DebugAction::SetRenderMode(RenderMode::Points))
);
}
#[test]
fn releasing_the_solo_modifier_restores_the_overlay_form() {
let mut controls = DebugControls::default();
controls.handle_key(DEBUG_MODIFIER, true);
controls.handle_key(KeyCode::ShiftLeft, true);
tap(&mut controls, KeyCode::KeyV);
controls.handle_key(KeyCode::ShiftLeft, false);
assert_eq!(
tap(&mut controls, KeyCode::KeyV),
Some(DebugAction::SetRenderMode(RenderMode::FilledPoints))
);
}
#[test]
fn switching_between_modes_does_not_pass_through_filled() {
let mut controls = DebugControls::default();
controls.handle_key(DEBUG_MODIFIER, true);
tap(&mut controls, KeyCode::KeyV);
assert_eq!(
tap(&mut controls, KeyCode::KeyB),
Some(DebugAction::SetRenderMode(RenderMode::FilledWireframe))
);
}
#[test]
fn repeating_the_chord_toggles_back_to_filled() {
let mut controls = DebugControls::default();
@ -67,3 +134,10 @@ fn unbound_key_under_the_modifier_is_ignored() {
controls.handle_key(DEBUG_MODIFIER, true);
assert_eq!(tap(&mut controls, KeyCode::KeyW), None);
}
#[test]
fn solo_modifier_alone_produces_no_action() {
let mut controls = DebugControls::default();
assert_eq!(controls.handle_key(KeyCode::ShiftLeft, true), None);
assert_eq!(tap(&mut controls, KeyCode::KeyV), None);
}