feat(client): add a stackable stats overlay toggle

This commit is contained in:
Serkyo 2026-08-02 00:44:10 +02:00
parent 17e7b465ce
commit d445a29e5e
3 changed files with 88 additions and 4 deletions

View file

@ -6,6 +6,7 @@
//! //!
//! - **F1 + V**: filled terrain with vertex points overlaid, showing where the mesher placed geometry without losing the surface. //! - **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. //! - **F1 + B**: filled terrain with the triangle edges overlaid, showing the size and shape of the emitted quads.
//! - **F1 + I**: the debug statistics panel.
//! //!
//! 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. //! 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.
//! //!
@ -20,6 +21,13 @@ 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. /// 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]; const SOLO_MODIFIER: [KeyCode; 2] = [KeyCode::ShiftLeft, KeyCode::ShiftRight];
const VERTEX_POINTS_OVERLAY_KEY: KeyCode = KeyCode::KeyV;
const WIREFRAME_OVERLAY_KEY: KeyCode = KeyCode::KeyB;
/// The key that, held alongside [`DEBUG_MODIFIER`], toggles the statistics panel.
const STATS_KEY: KeyCode = KeyCode::KeyI;
/// A debug operation requested by the input layer, applied by the caller. /// 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. /// 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.
@ -27,6 +35,8 @@ const SOLO_MODIFIER: [KeyCode; 2] = [KeyCode::ShiftLeft, KeyCode::ShiftRight];
pub(crate) enum DebugAction { pub(crate) enum DebugAction {
/// Applies the given rasterisation mode to the renderer. /// Applies the given rasterisation mode to the renderer.
SetRenderMode(RenderMode), SetRenderMode(RenderMode),
/// Enables or disables emission of the statistics panel.
SetStatsOverlay(bool),
} }
/// Owns debug-only input state and translates key events into [`DebugAction`]s. /// Owns debug-only input state and translates key events into [`DebugAction`]s.
@ -38,6 +48,8 @@ pub(crate) struct DebugControls {
solo_held: bool, solo_held: bool,
/// The rasterisation mode most recently requested, used to make each chord a toggle back to [`RenderMode::Filled`]. /// The rasterisation mode most recently requested, used to make each chord a toggle back to [`RenderMode::Filled`].
render_mode: RenderMode, render_mode: RenderMode,
/// Whether the statistics panel is being emitted.
stats_enabled: bool,
} }
impl DebugControls { impl DebugControls {
@ -60,6 +72,12 @@ impl DebugControls {
return None; return None;
} }
// The statistics chord is resolved before the raster table so the two axes never contend for a key. The solo modifier selects between overlaid and standalone geometry and has no meaning for a panel that draws none, so it is ignored here.
if code == STATS_KEY {
self.stats_enabled = !self.stats_enabled;
return Some(DebugAction::SetStatsOverlay(self.stats_enabled));
}
let requested = render_mode_for_key(code, self.solo_held)?; 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. // Re-pressing the chord for the active mode returns to the normal path, so a single chord both enables and disables its mode.
@ -78,10 +96,10 @@ impl DebugControls {
/// This is the single table a new rasterisation debug mode is added to: one key, one overlaid form, one solo form. /// 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> { const fn render_mode_for_key(code: KeyCode, solo: bool) -> Option<RenderMode> {
match (code, solo) { match (code, solo) {
(KeyCode::KeyV, false) => Some(RenderMode::FilledPoints), (VERTEX_POINTS_OVERLAY_KEY, false) => Some(RenderMode::FilledPoints),
(KeyCode::KeyV, true) => Some(RenderMode::Points), (VERTEX_POINTS_OVERLAY_KEY, true) => Some(RenderMode::Points),
(KeyCode::KeyB, false) => Some(RenderMode::FilledWireframe), (WIREFRAME_OVERLAY_KEY, false) => Some(RenderMode::FilledWireframe),
(KeyCode::KeyB, true) => Some(RenderMode::Wireframe), (WIREFRAME_OVERLAY_KEY, true) => Some(RenderMode::Wireframe),
_ => None, _ => None,
} }
} }

View file

@ -72,6 +72,8 @@ struct App {
last_center: Option<shared::world::ChunkPos>, last_center: Option<shared::world::ChunkPos>,
/// Streams chunk meshes in and out around the camera. `None` until the renderer is initialised on resume. /// Streams chunk meshes in and out around the camera. `None` until the renderer is initialised on resume.
chunks: Option<chunks::ChunkManager>, chunks: Option<chunks::ChunkManager>,
/// Whether the debug statistics panel is being emitted. Collection is unconditional; only emission is gated on this.
stats_overlay: bool,
} }
impl Default for App { impl Default for App {
@ -92,6 +94,7 @@ impl Default for App {
connected: false, connected: false,
last_center: None, last_center: None,
chunks: None, chunks: None,
stats_overlay: false,
} }
} }
} }
@ -108,6 +111,10 @@ impl App {
info!(?mode, "render mode toggled"); info!(?mode, "render mode toggled");
} }
} }
debug::DebugAction::SetStatsOverlay(enabled) => {
self.stats_overlay = enabled;
info!(enabled, "statistics overlay toggled");
}
} }
} }
} }

View file

@ -141,3 +141,62 @@ fn solo_modifier_alone_produces_no_action() {
assert_eq!(controls.handle_key(KeyCode::ShiftLeft, true), None); assert_eq!(controls.handle_key(KeyCode::ShiftLeft, true), None);
assert_eq!(tap(&mut controls, KeyCode::KeyV), None); assert_eq!(tap(&mut controls, KeyCode::KeyV), None);
} }
#[test]
fn the_stats_chord_toggles_the_overlay_on_and_off() {
let mut controls = DebugControls::default();
controls.handle_key(DEBUG_MODIFIER, true);
assert_eq!(
tap(&mut controls, STATS_KEY),
Some(DebugAction::SetStatsOverlay(true))
);
assert_eq!(
tap(&mut controls, STATS_KEY),
Some(DebugAction::SetStatsOverlay(false))
);
}
#[test]
fn the_stats_chord_leaves_the_render_mode_untouched() {
let mut controls = DebugControls::default();
controls.handle_key(DEBUG_MODIFIER, true);
tap(&mut controls, KeyCode::KeyV);
tap(&mut controls, STATS_KEY);
// The raster axis must survive a toggle on the statistics axis; a single-enum design would have reset it.
assert_eq!(controls.render_mode, RenderMode::FilledPoints);
}
#[test]
fn a_raster_chord_leaves_the_stats_overlay_untouched() {
let mut controls = DebugControls::default();
controls.handle_key(DEBUG_MODIFIER, true);
tap(&mut controls, STATS_KEY);
assert_eq!(
tap(&mut controls, KeyCode::KeyB),
Some(DebugAction::SetRenderMode(RenderMode::FilledWireframe))
);
assert!(controls.stats_enabled);
}
#[test]
fn the_solo_modifier_does_not_change_the_stats_chord() {
let mut controls = DebugControls::default();
controls.handle_key(DEBUG_MODIFIER, true);
controls.handle_key(KeyCode::ShiftLeft, true);
assert_eq!(
tap(&mut controls, STATS_KEY),
Some(DebugAction::SetStatsOverlay(true))
);
}
#[test]
fn the_stats_chord_requires_the_debug_modifier() {
let mut controls = DebugControls::default();
assert_eq!(tap(&mut controls, STATS_KEY), None);
assert!(!controls.stats_enabled);
}