Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338use bevy::input::mouse::{MouseMotion, MouseWheel};use bevy::prelude::*;use bevy_panorbit_camera::PanOrbitCamera;
use crate::db::{DbChannel, DbReadRequest};use crate::helix::{HelixHierarchy, HelixState, compute_focal_point};use crate::ingest::{HistoryQueryState, JetstreamEventMarker, LoadedRanges, TimeWindow};
const SCROLL_SENSITIVITY: f32 = 0.5;const ZOOM_INTERPOLATION_FACTOR: f32 = 5.0;const DRAG_SENSITIVITY: f32 = 0.0005;const INERTIA_DECAY_RATE: f32 = 3.0;const VELOCITY_EPSILON: f32 = 0.0001;
#[derive(Resource, Debug, Default)]pub struct DragState { pub is_dragging: bool, pub velocity: f32,}
#[derive(Resource, Debug)]pub struct ZoomConfig { pub zoom_button: MouseButton, pub drag_sensitivity: f32,}
impl Default for ZoomConfig { fn default() -> Self { Self { zoom_button: MouseButton::Other(8), // forward side button (trackball) drag_sensitivity: 0.02, } }}
/// Scroll wheel → zoom level.pub fn scroll_zoom_level( mut scroll_events: MessageReader<MouseWheel>, mut state: ResMut<HelixState>, hierarchy: Res<HelixHierarchy>,) { if hierarchy.levels.is_empty() { return; }
let mut delta = 0.0; for event in scroll_events.read() { delta += event.y; }
if delta != 0.0 { let delta_level = delta * SCROLL_SENSITIVITY; let new_target = state.target_level - delta_level; let max_level = (hierarchy.levels.len() - 1) as f32; state.target_level = new_target.clamp(0.0, max_level); }}
/// Right-drag pans through time with inertia.pub fn drag_pan_time( mut motion_events: MessageReader<MouseMotion>, mouse_buttons: Res<ButtonInput<MouseButton>>, mut state: ResMut<HelixState>, mut drag: ResMut<DragState>, time: Res<Time>,) { let right_pressed = mouse_buttons.pressed(MouseButton::Right); let mut frame_delta = 0.0;
if right_pressed { for event in motion_events.read() { frame_delta += event.delta.y + event.delta.x * 0.5; }
drag.is_dragging = true; state.auto_follow = false;
let time_shift = frame_delta * DRAG_SENSITIVITY; state.focal_time -= time_shift;
let avg_factor = 0.7; drag.velocity = drag.velocity * avg_factor + time_shift * (1.0 - avg_factor); } else { if drag.is_dragging { drag.is_dragging = false; }
let delta_secs = time.delta_secs(); state.focal_time -= drag.velocity; drag.velocity *= (-INERTIA_DECAY_RATE * delta_secs).exp();
if drag.velocity.abs() < VELOCITY_EPSILON { drag.velocity = 0.0; } }}
/// Side-button + vertical drag → zoom level.pub fn button_zoom_level( mut motion_events: MessageReader<MouseMotion>, mouse_buttons: Res<ButtonInput<MouseButton>>, zoom_config: Res<ZoomConfig>, mut state: ResMut<HelixState>, hierarchy: Res<HelixHierarchy>,) { if !mouse_buttons.pressed(zoom_config.zoom_button) { return; }
if hierarchy.levels.is_empty() { return; }
let mut delta_y = 0.0; for event in motion_events.read() { delta_y += event.delta.y; }
if delta_y != 0.0 { let delta_level = delta_y * zoom_config.drag_sensitivity; let new_target = state.target_level - delta_level; let max_level = (hierarchy.levels.len() - 1) as f32; state.target_level = new_target.clamp(0.0, max_level); }}
/// Smooth interpolation of zoom level.pub fn interpolate_zoom_level(mut state: ResMut<HelixState>, time: Res<Time>) { let lerp_factor = (ZOOM_INTERPOLATION_FACTOR * time.delta_secs()).clamp(0.0, 1.0); state.interpolated_level = state.interpolated_level * (1.0 - lerp_factor) + state.target_level * lerp_factor; state.active_level = state.interpolated_level.round() as usize;}
/// Space: toggle auto-follow (pause/unpause).pub fn toggle_pause(keys: Res<ButtonInput<KeyCode>>, mut state: ResMut<HelixState>) { if keys.just_pressed(KeyCode::Space) { state.auto_follow = !state.auto_follow; info!("auto_follow: {}", state.auto_follow); }}
const SLOW_TIME_SCALE: f32 = 0.1;
/// S: toggle slow time (1.0 ↔ 0.1).pub fn toggle_slow_time(keys: Res<ButtonInput<KeyCode>>, mut state: ResMut<HelixState>) { if keys.just_pressed(KeyCode::KeyS) { if state.time_scale >= 1.0 { state.time_scale = SLOW_TIME_SCALE; info!("time_scale: {} (slow)", state.time_scale); } else { state.time_scale = 1.0; info!("time_scale: 1.0 (normal)"); } }}
/// Track latest data. Snaps at full speed, lerps when slowed or catching up.pub fn auto_follow_latest( mut state: ResMut<HelixState>, window: Res<crate::ingest::TimeWindow>, time: Res<Time>,) { if !state.auto_follow || !window.is_initialized { return; }
let dt = time.delta_secs(); let gap = window.latest_t - state.focal_time;
if state.time_scale >= 1.0 { // Full speed: snap when close, fast lerp when catching up from pause. if gap.abs() < 0.05 { state.focal_time = window.latest_t; } else { let factor = (3.0 * dt).clamp(0.0, 1.0); state.focal_time += gap * factor; } } else { // Slow mode: advance at time_scale fraction of real time. // 1 t-unit = 1 minute, so real-time advance per second = 1/60 t-units. // At time_scale=0.1, we advance at 1/600 t-units per second. let advance = state.time_scale * dt / 60.0; state.focal_time += advance; }}
/// Recenter camera on the helix focal point.pub fn update_camera_focus( state: Res<HelixState>, hierarchy: Res<HelixHierarchy>, mut cameras: Query<&mut PanOrbitCamera>,) { let target = compute_focal_point(&state, &hierarchy);
for mut cam in cameras.iter_mut() { cam.target_focus = target; }}
/// Pan past loaded range -> trigger SQLite query (5 min either side of focal_time).pub fn check_pan_triggers_history_load( state: Res<HelixState>, time_window: Res<TimeWindow>, loaded_ranges: Res<LoadedRanges>, mut query_state: ResMut<HistoryQueryState>, db_channel: Res<DbChannel>,) { if state.auto_follow { return; }
if !time_window.is_initialized { return; }
if query_state.pending.is_some() { return; }
let focal_us = (state.focal_time as f64 * TimeWindow::MICROS_PER_TURN) as i64 + time_window.anchor_us; const HALF_WINDOW_US: i64 = 300_000_000; let query_start = focal_us - HALF_WINDOW_US; let query_end = focal_us + HALF_WINDOW_US;
if loaded_ranges.is_covered(query_start, query_end) { return; }
let (resp_tx, resp_rx) = crossbeam_channel::bounded::<Vec<Vec<rusqlite::types::Value>>>(1);
if db_channel .reader .send(DbReadRequest::QueryTimeRange { start_us: query_start, end_us: query_end, response_tx: resp_tx, }) .is_err() { warn!("check_pan_triggers_history_load: failed to send QueryTimeRange"); return; }
debug!( "check_pan_triggers_history_load: querying range [{}, {}] for focal_time={}", query_start, query_end, state.focal_time );
query_state.pending = Some(resp_rx); query_state.pending_range = Some((query_start, query_end));}
#[derive(Resource, Default)]pub struct SelectedEvent { pub entity: Option<Entity>, pub previous_entity: Option<Entity>,}
/// Cached cursor position (Wayland doesn't provide window.cursor_position).#[derive(Resource, Default)]pub struct LastCursorPosition(pub Option<Vec2>);
pub fn track_cursor( mut cursor_events: MessageReader<bevy::window::CursorMoved>, mut last_pos: ResMut<LastCursorPosition>,) { for event in cursor_events.read() { last_pos.0 = Some(event.position); }}
pub fn pick_event( cameras: Query<(&Camera, &GlobalTransform), With<PanOrbitCamera>>, events: Query<(Entity, &Transform), With<JetstreamEventMarker>>, mut selected: ResMut<SelectedEvent>, cursor: Res<LastCursorPosition>,) { let Some(cursor_pos) = cursor.0 else { return; }; let Ok((camera, camera_transform)) = cameras.single() else { return; };
let Ok(ray) = camera.viewport_to_world(camera_transform, cursor_pos) else { return; };
let mut closest: Option<(Entity, f32)> = None; let threshold = 0.15_f32;
for (entity, transform) in events.iter() { let to_point = transform.translation - ray.origin; let projected = to_point.dot(*ray.direction); if projected < 0.0 { continue; } let closest_on_ray = ray.origin + *ray.direction * projected; let distance = (transform.translation - closest_on_ray).length();
if distance < threshold { match closest { None => closest = Some((entity, distance)), Some((_, best)) if distance < best => closest = Some((entity, distance)), _ => {} } } }
selected.entity = closest.map(|(e, _)| e);}
/// Gizmo highlight on selected event.pub fn highlight_selected( mut gizmos: Gizmos, selected: Res<SelectedEvent>, events: Query<&Transform, With<JetstreamEventMarker>>,) { let Some(entity) = selected.entity else { return; };
let Ok(transform) = events.get(entity) else { return; };
let radius = 0.02; let color = Color::srgb(1.0, 0.95, 0.0);
gizmos.sphere( Isometry3d::from_translation(transform.translation), radius, color, );}