qualia_core_db/render/
navigation.rs1use crate::render::camera::CameraState;
4use crate::render::telemetry::ObserverStandpoint;
5use crate::tensor::buffer_export::read_tensor_at;
6
7pub const PICK_SENTINEL: u32 = u32::MAX;
9
10pub const FLY_TO_FRAMES: u32 = 24;
12
13pub const Q_COLLAPSED_EPS: f32 = 0.001;
15
16#[derive(Clone, Copy, Debug, Default, PartialEq)]
18pub struct CameraFlyTo {
19 pub target: CameraState,
20 pub remaining: u32,
21}
22
23impl CameraFlyTo {
24 #[inline]
25 pub fn is_active(&self) -> bool {
26 self.remaining > 0
27 }
28
29 pub fn start_toward(target: CameraState) -> Self {
30 Self {
31 target: target.clamped(),
32 remaining: FLY_TO_FRAMES,
33 }
34 }
35
36 pub fn advance(&mut self, current: CameraState) -> CameraState {
38 if self.remaining == 0 {
39 return current.clamped();
40 }
41 let t = 1.0 - (self.remaining as f32 / FLY_TO_FRAMES as f32);
42 let blended = lerp_camera(current, self.target, t.clamp(0.0, 1.0));
43 self.remaining = self.remaining.saturating_sub(1);
44 blended
45 }
46}
47
48#[inline]
50pub fn camera_frame_node(node: [f32; 3]) -> CameraState {
51 let [x, y, z] = node;
52 let dist = (x * x + y * y + z * z).sqrt().max(0.05);
53 let yaw = x.atan2(z);
54 let pitch = (y / dist).clamp(-1.0, 1.0).asin();
55 let zoom = (dist * 1.75).clamp(0.35, 48.0);
56 CameraState { yaw, pitch, zoom }.clamped()
57}
58
59#[inline]
60pub fn lerp_camera(a: CameraState, b: CameraState, t: f32) -> CameraState {
61 CameraState {
62 yaw: a.yaw + (b.yaw - a.yaw) * t,
63 pitch: a.pitch + (b.pitch - a.pitch) * t,
64 zoom: a.zoom + (b.zoom - a.zoom) * t,
65 }
66 .clamped()
67}
68
69pub fn cpu_pick_node_at(
71 tensor: &[u8],
72 canvas_w: f64,
73 canvas_h: f64,
74 pick_x: f64,
75 pick_y: f64,
76 yaw: f32,
77 standpoint: &ObserverStandpoint,
78) -> Option<u32> {
79 let count = crate::tensor::buffer_export::tensor_node_count(tensor).ok()?;
80 if count == 0 {
81 return None;
82 }
83
84 let mut best: Option<(u32, f64)> = None;
85 for i in 0..count {
86 let Ok(t) = read_tensor_at(tensor, i) else {
87 continue;
88 };
89 if !standpoint.temporal_visible(t.t) {
90 continue;
91 }
92 let (px, py, _) = project_xyz_canvas(t.x, t.y, t.z, canvas_w, canvas_h, yaw as f64);
93 let dx = px - pick_x;
94 let dy = py - pick_y;
95 let hit_r = 8.0 + t.alpha as f64 * 6.0;
96 let d2 = dx * dx + dy * dy;
97 if d2 > hit_r * hit_r {
98 continue;
99 }
100 if best.map_or(true, |(_, bd)| d2 < bd) {
101 best = Some((i as u32, d2));
102 }
103 }
104 best.map(|(idx, _)| idx)
105}
106
107#[inline]
108fn project_xyz_canvas(x: f32, y: f32, z: f32, w: f64, h: f64, yaw: f64) -> (f64, f64, f32) {
109 let cx = yaw.cos() as f32;
110 let sx = yaw.sin() as f32;
111 let xr = x * cx + z * sx;
112 let zr = -x * sx + z * cx;
113 let depth = (1.0 / (1.0 + zr * 0.35)).clamp(0.2, 1.0);
114 let scale = 0.42 * w.min(h) * depth as f64;
115 let px = w * 0.5 + xr as f64 * scale;
116 let py = h * 0.5 - y as f64 * scale;
117 (px, py, depth)
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn camera_frame_node_produces_finite_orbit() {
126 let cam = camera_frame_node([1.0, 0.5, -2.0]);
127 assert!(cam.yaw.is_finite());
128 assert!(cam.pitch.is_finite());
129 assert!(cam.zoom.is_finite());
130 }
131
132 #[test]
133 fn fly_to_converges_toward_target() {
134 let target = camera_frame_node([0.0, 1.0, 2.0]);
135 let mut fly = CameraFlyTo::start_toward(target);
136 let mut cam = CameraState::default();
137 for _ in 0..FLY_TO_FRAMES {
138 cam = fly.advance(cam);
139 }
140 assert!((cam.yaw - target.yaw).abs() < 0.05);
141 }
142}