Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 134 additions & 6 deletions crates/reco-cli/src/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
//! - `[`/`]`: seek backward/forward 5 seconds, Home: restart
//! - Arrows / mouse drag: pan (yaw/pitch)
//! - +/- / scroll: zoom (FOV)
//! - F: toggle free-fly camera (then WASD move, E/C up/down, Shift boost,
//! mouse-drag look) - move the virtual camera through the 3D scene
//! - Q / Escape: quit
//!
//! This module is intentionally CLI-only. The rendering is already handled by
Expand Down Expand Up @@ -46,6 +48,10 @@ const FOV_MIN: f32 = 20.0;
const FOV_MAX: f32 = 150.0;
/// Default FOV at startup (degrees).
const FOV_DEFAULT: f32 = 75.0;
/// Free-fly camera movement speed (scene units per second).
const FLY_SPEED: f32 = 0.6;
/// Free-fly speed multiplier while Shift is held.
const FLY_BOOST: f32 = 4.0;
/// Number of frames to skip on P key press.
const FRAME_SKIP_COUNT: usize = 30;
/// Number of seconds to seek on `[`/`]` key press.
Expand Down Expand Up @@ -203,6 +209,10 @@ pub fn run_preview(
fps_rational,
total_frames,
pending_seek: None,
fly_mode: false,
keys_down: std::collections::HashSet::new(),
last_move_time: Instant::now(),
rmb_dragging: false,
};

event_loop.run_app(&mut app)?;
Expand Down Expand Up @@ -260,6 +270,16 @@ struct App {
/// Coalesced seek target. Multiple rapid key presses accumulate here;
/// only the final value is executed (in about_to_wait).
pending_seek: Option<u64>,
// -- Free-fly camera (debug navigation) --
/// When true, WASD/E/C move the virtual camera through 3D space and
/// mouse-drag looks around; the normal letter hotkeys are suspended.
fly_mode: bool,
/// Movement keys currently held (WASD / E / C / Shift) for continuous fly.
keys_down: std::collections::HashSet<winit::keyboard::KeyCode>,
/// Last free-fly integration tick, for frame-rate-independent movement.
last_move_time: Instant,
/// Right mouse button held (free-look in fly mode).
rmb_dragging: bool,
}

impl App {
Expand Down Expand Up @@ -568,7 +588,59 @@ impl ApplicationHandler for App {
}
WindowEvent::KeyboardInput { event, .. } => {
use winit::keyboard::{KeyCode, PhysicalKey};
if event.state == winit::event::ElementState::Pressed {
let pressed = event.state == winit::event::ElementState::Pressed;

// F toggles free-fly mode (camera translation via WASD/E/C +
// mouse-drag look). Available in any mode.
if pressed && event.physical_key == PhysicalKey::Code(KeyCode::KeyF) {
self.fly_mode = !self.fly_mode;
self.keys_down.clear();
if self.fly_mode {
self.last_move_time = Instant::now();
println!(
"Fly mode ON - WASD move, E/C up/down, Shift boost, mouse-drag look, F to exit"
);
} else {
if !self.playing {
event_loop.set_control_flow(ControlFlow::Wait);
}
println!("Fly mode OFF");
}
return;
}

// In fly mode, WASD/E/C/Shift drive camera movement (held-key
// state) and suspend their normal letter-hotkey meanings.
if self.fly_mode
&& let PhysicalKey::Code(code) = event.physical_key
&& matches!(
code,
KeyCode::KeyW
| KeyCode::KeyA
| KeyCode::KeyS
| KeyCode::KeyD
| KeyCode::KeyE
| KeyCode::KeyC
| KeyCode::ShiftLeft
| KeyCode::ShiftRight
)
{
if pressed {
if self.keys_down.is_empty() {
self.last_move_time = Instant::now();
}
self.keys_down.insert(code);
event_loop.set_control_flow(ControlFlow::Poll);
} else {
self.keys_down.remove(&code);
if self.keys_down.is_empty() && !self.playing {
event_loop.set_control_flow(ControlFlow::Wait);
}
}
return;
}

if pressed {
match event.physical_key {
PhysicalKey::Code(KeyCode::Escape | KeyCode::KeyQ) => {
if self.recording.is_some() {
Expand Down Expand Up @@ -770,21 +842,32 @@ impl ApplicationHandler for App {
WindowEvent::MouseInput { state, button, .. } => {
use winit::event::ElementState;
use winit::event::MouseButton;
if button == MouseButton::Left {
if button == MouseButton::Left || button == MouseButton::Right {
let pressed = state == ElementState::Pressed;
self.mouse_dragging = pressed;
// Left drag and right drag both look around (yaw/pitch).
if button == MouseButton::Left {
self.mouse_dragging = pressed;
} else {
self.rmb_dragging = pressed;
}
if pressed {
// Capture start position - first CursorMoved will anchor here
self.last_mouse_pos = None;
} else {
self.last_mouse_pos = None;
if !self.playing {
if !self.mouse_dragging
&& !self.rmb_dragging
&& self.keys_down.is_empty()
&& !self.playing
{
event_loop.set_control_flow(ControlFlow::Wait);
}
}
}
}
WindowEvent::CursorMoved { position, .. } if self.mouse_dragging => {
WindowEvent::CursorMoved { position, .. }
if self.mouse_dragging || self.rmb_dragging =>
{
if let Some((prev_x, prev_y)) = self.last_mouse_pos {
let dx = (position.x - prev_x) as f32;
let dy = (position.y - prev_y) as f32;
Expand Down Expand Up @@ -918,7 +1001,52 @@ impl ApplicationHandler for App {
self.needs_redraw = true;
}

if !self.playing && !smoothing_active {
// Free-fly camera translation: integrate held movement keys into the
// virtual camera position, frame-rate independent via dt.
if self.fly_mode && !self.keys_down.is_empty() {
use winit::keyboard::KeyCode;
let dt = self.last_move_time.elapsed().as_secs_f32().min(0.05);
let k = &self.keys_down;
let mut mv = [0.0_f32; 3]; // [right, up, forward]
if k.contains(&KeyCode::KeyD) {
mv[0] += 1.0;
}
if k.contains(&KeyCode::KeyA) {
mv[0] -= 1.0;
}
if k.contains(&KeyCode::KeyE) {
mv[1] += 1.0;
}
if k.contains(&KeyCode::KeyC) {
mv[1] -= 1.0;
}
if k.contains(&KeyCode::KeyW) {
mv[2] += 1.0;
}
if k.contains(&KeyCode::KeyS) {
mv[2] -= 1.0;
}
if mv != [0.0; 3] {
let boost = if k.contains(&KeyCode::ShiftLeft) || k.contains(&KeyCode::ShiftRight) {
FLY_BOOST
} else {
1.0
};
let step = FLY_SPEED * boost * dt;
let render = self.pose.render_pose(self.rig_tilt);
if let Some(r) = &mut self.renderer {
r.pipeline_mut().fly_camera(
[mv[0] * step, mv[1] * step, mv[2] * step],
render.yaw,
render.pitch,
);
}
self.needs_redraw = true;
}
}
self.last_move_time = Instant::now();

if !self.playing && !smoothing_active && self.keys_down.is_empty() {
return;
}

Expand Down
45 changes: 45 additions & 0 deletions crates/reco-core/src/render/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,51 @@ impl StitchPipeline {
self.viewport.lens_correction_amount = amount.clamp(0.0, 1.0);
}

/// Current virtual camera position `[x, y, z]` in scene space.
pub fn camera_position(&self) -> [f32; 3] {
self.scene.camera_position
}

/// Override the virtual camera position (e.g. to reset free-fly).
///
/// Takes effect on the next render (the eye is read from the scene each
/// frame). The position is never allowed to reach the scene origin,
/// where the look-toward-origin basis would be undefined.
pub fn set_camera_position(&mut self, pos: [f32; 3]) {
let norm = (pos[0] * pos[0] + pos[1] * pos[1] + pos[2] * pos[2]).sqrt();
if norm > 1e-2 {
self.scene.camera_position = pos;
}
}

/// Translate the virtual camera in its current view frame (free-fly).
///
/// `local` is `[right, up, forward]` in scene-space distance units,
/// evaluated at the given `yaw`/`pitch` so movement follows where the
/// camera looks. The basis matches `view_matrix`'s yaw-around-up,
/// pitch-around-right convention (rig tilt/roll are ignored here; this
/// is a debug/preview navigation aid, not a render path). Vertical
/// (`up`) uses world up so it stays level regardless of pitch.
pub fn fly_camera(&mut self, local: [f32; 3], yaw: f32, pitch: f32) {
use crate::projection::VirtualCamera;
use nalgebra::{Unit, UnitQuaternion};

let cam = VirtualCamera::new(&self.scene.camera_position);
let world_up = VirtualCamera::world_up();
let yaw_q = UnitQuaternion::from_axis_angle(&Unit::new_normalize(world_up), yaw);
let right = yaw_q * cam.base_right;
let pitch_q = UnitQuaternion::from_axis_angle(&Unit::new_normalize(right), pitch);
let forward = (pitch_q * yaw_q) * cam.base_forward;

let delta = right * local[0] + world_up * local[1] + forward * local[2];
let next = [
self.scene.camera_position[0] + delta.x,
self.scene.camera_position[1] + delta.y,
self.scene.camera_position[2] + delta.z,
];
self.set_camera_position(next);
}

/// Update calibration parameters. Recomputes [`SceneGeometry`] from the
/// new layout. Takes effect on the next render call (uniforms are rebuilt
/// each frame from the stored calibration and scene).
Expand Down
Loading
Loading