Skip to main content

khora_core/ui/editor/
camera.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Editor camera with orbit / pan / zoom controls.
16//!
17//! This is a backend-agnostic camera controller. The concrete input
18//! handling is performed by the editor application, which translates
19//! [`InputEvent`](crate::platform) into the methods exposed here.
20
21use crate::math::{Mat4, Vec3, FRAC_PI_4};
22use crate::renderer::api::resource::view::ViewInfo;
23
24/// An orbit camera controller for the editor viewport.
25///
26/// The camera orbits around a `target` point. Middle-click drag orbits,
27/// Shift+middle-click pans, scroll zooms.
28#[derive(Debug, Clone)]
29pub struct EditorCamera {
30    /// Look-at target (orbit center).
31    pub target: Vec3,
32    /// Horizontal angle around Y axis (radians).
33    pub yaw: f32,
34    /// Vertical angle from the XZ plane (radians), clamped to ±89°.
35    pub pitch: f32,
36    /// Distance from target.
37    pub distance: f32,
38    /// Vertical field of view (radians).
39    pub fov_y: f32,
40    /// Near plane.
41    pub near: f32,
42    /// Far plane.
43    pub far: f32,
44    /// Orbit speed multiplier.
45    pub orbit_speed: f32,
46    /// Pan speed multiplier.
47    pub pan_speed: f32,
48    /// Zoom speed multiplier.
49    pub zoom_speed: f32,
50    /// Minimum orbit distance.
51    pub min_distance: f32,
52    /// Maximum orbit distance.
53    pub max_distance: f32,
54}
55
56impl Default for EditorCamera {
57    fn default() -> Self {
58        Self {
59            target: Vec3::ZERO,
60            yaw: 0.4,    // ~23° from front
61            pitch: -0.4, // ~23° looking down
62            distance: 10.0,
63            fov_y: FRAC_PI_4, // 45°
64            near: 0.1,
65            far: 1000.0,
66            orbit_speed: 0.005,
67            pan_speed: 0.01,
68            zoom_speed: 1.0,
69            min_distance: 0.5,
70            max_distance: 500.0,
71        }
72    }
73}
74
75impl EditorCamera {
76    /// Computes the camera's world-space position from orbit parameters.
77    pub fn position(&self) -> Vec3 {
78        let x = self.distance * self.pitch.cos() * self.yaw.sin();
79        let y = self.distance * (-self.pitch).sin();
80        let z = self.distance * self.pitch.cos() * self.yaw.cos();
81        self.target + Vec3::new(x, y, z)
82    }
83
84    /// Orbit by a screen-space delta (pixels).
85    pub fn orbit(&mut self, delta_x: f32, delta_y: f32) {
86        self.yaw += delta_x * self.orbit_speed;
87        self.pitch -= delta_y * self.orbit_speed;
88        // Clamp pitch to avoid gimbal lock
89        let limit = 89.0_f32.to_radians();
90        self.pitch = self.pitch.clamp(-limit, limit);
91    }
92
93    /// Pan the target in the camera's local XY plane.
94    pub fn pan(&mut self, delta_x: f32, delta_y: f32) {
95        let right = self.right();
96        let up = self.up();
97        let speed = self.pan_speed * self.distance * 0.1;
98        self.target = self.target - right * delta_x * speed + up * delta_y * speed;
99    }
100
101    /// Zoom by a scroll delta (positive = closer).
102    pub fn zoom(&mut self, delta: f32) {
103        self.distance -= delta * self.zoom_speed * self.distance * 0.1;
104        self.distance = self.distance.clamp(self.min_distance, self.max_distance);
105    }
106
107    /// Focus the camera on a given world-space point.
108    pub fn focus_on(&mut self, point: Vec3) {
109        self.target = point;
110    }
111
112    /// The camera's forward direction (from camera to target).
113    pub fn forward(&self) -> Vec3 {
114        (self.target - self.position()).normalize()
115    }
116
117    /// The camera's right direction.
118    pub fn right(&self) -> Vec3 {
119        self.forward().cross(Vec3::Y).normalize()
120    }
121
122    /// The camera's up direction.
123    pub fn up(&self) -> Vec3 {
124        self.right().cross(self.forward()).normalize()
125    }
126
127    /// Builds a [`ViewInfo`] for the given viewport dimensions.
128    pub fn view_info(&self, width: f32, height: f32) -> ViewInfo {
129        let pos = self.position();
130        let view_matrix = Mat4::look_at_rh(pos, self.target, Vec3::Y).unwrap_or(Mat4::IDENTITY);
131        let aspect = if height > 0.0 { width / height } else { 1.0 };
132        let projection_matrix = Mat4::perspective_rh_zo(self.fov_y, aspect, self.near, self.far);
133
134        ViewInfo {
135            view_matrix,
136            projection_matrix,
137            camera_position: pos,
138        }
139    }
140
141    /// Compute a world-space ray from a viewport pixel position.
142    ///
143    /// `x` and `y` are in pixels (top-left origin), `width`/`height`
144    /// are the viewport dimensions in pixels.
145    pub fn screen_to_ray(&self, x: f32, y: f32, width: f32, height: f32) -> crate::physics::Ray {
146        let view_info = self.view_info(width, height);
147        let vp = view_info.view_projection_matrix();
148        let inv_vp = vp.inverse().unwrap_or(Mat4::IDENTITY);
149
150        // Normalised device coords ([-1, 1] range, Y flipped for screen space)
151        let ndc_x = (2.0 * x / width) - 1.0;
152        let ndc_y = 1.0 - (2.0 * y / height);
153
154        let near_ndc = crate::math::Vec4::new(ndc_x, ndc_y, 0.0, 1.0);
155        let far_ndc = crate::math::Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
156
157        let near_world = inv_vp * near_ndc;
158        let far_world = inv_vp * far_ndc;
159
160        let near_pt = near_world.truncate() / near_world.w;
161        let far_pt = far_world.truncate() / far_world.w;
162
163        let direction = (far_pt - near_pt).normalize();
164
165        crate::physics::Ray {
166            origin: near_pt,
167            direction,
168        }
169    }
170}