Skip to main content

rustapi/
editcamera.rs

1use crate::prelude::*;
2use theframework::prelude::*;
3
4use rusterix::{D3Camera, D3FirstPCamera, D3IsoCamera, D3OrbitCamera, Rusterix};
5
6pub enum CustomMoveAction {
7    Forward,
8    Backward,
9    Left,
10    Right,
11}
12
13pub struct EditCamera {
14    pub orbit_camera: D3OrbitCamera,
15    pub iso_camera: D3IsoCamera,
16    pub firstp_camera: D3FirstPCamera,
17
18    pub move_action: Option<CustomMoveAction>,
19    last_mouse: Option<Vec2<i32>>,
20}
21
22#[allow(clippy::new_without_default)]
23impl EditCamera {
24    pub fn new() -> Self {
25        Self {
26            orbit_camera: D3OrbitCamera::new(),
27            iso_camera: D3IsoCamera::new(),
28            firstp_camera: D3FirstPCamera::new(),
29
30            move_action: None,
31            last_mouse: None,
32        }
33    }
34
35    pub fn setup_toolbar(
36        &mut self,
37        layout: &mut dyn TheHLayoutTrait,
38        _ctx: &mut TheContext,
39        _project: &mut Project,
40        server_ctx: &mut ServerContext,
41    ) {
42        let mut view_switch = TheGroupButton::new(TheId::named("Editor View Switch"));
43        view_switch.add_text_status("2D".to_string(), "Edit the map in 2D.".to_string());
44        if cfg!(target_os = "macos") {
45            view_switch.add_text_status(
46                    "Orbit".to_string(),
47                    "Edit the map with a 3D orbit camera. Scroll to move. Cmd + Scroll to zoom. Alt + Scroll to rotate.".to_string(),
48                );
49        } else {
50            view_switch.add_text_status(
51                    "Orbit".to_string(),
52                    "Edit the map with a 3D orbit camera. Scroll to move. Ctrl + Scroll to zoom. Alt + Scroll to rotate.".to_string(),
53                );
54        }
55        if cfg!(target_os = "macos") {
56            view_switch.add_text_status(
57                "Iso".to_string(),
58                "Edit the map in 3D isometric view. Scroll to move. Cmd + Scroll to zoom. "
59                    .to_string(),
60            );
61        } else {
62            view_switch.add_text_status(
63                "Iso".to_string(),
64                "Edit the map in 3D isometric view. Scroll to move. Ctrl + Scroll to zoom."
65                    .to_string(),
66            );
67        }
68        view_switch.add_text_status(
69            "FirstP".to_string(),
70            "Edit the map in 3D first person view.".to_string(),
71        );
72        view_switch.set_index(server_ctx.editor_view_mode.to_index());
73        layout.add_widget(Box::new(view_switch));
74        layout.set_reverse_index(Some(1));
75    }
76
77    /// Update client camera
78    pub fn update_camera(
79        &mut self,
80        region: &mut Region,
81        server_ctx: &mut ServerContext,
82        rusterix: &mut Rusterix,
83    ) {
84        if server_ctx.editor_view_mode == EditorViewMode::FirstP {
85            rusterix.client.camera_d3 = Box::new(self.firstp_camera.clone());
86
87            let height = region
88                .map
89                .terrain
90                .sample_height(region.editing_position_3d.x, region.editing_position_3d.z)
91                + 1.5;
92
93            // let h = region.map.terrain.get_height_unprocessed(
94            //     region.editing_position_3d.x as i32,
95            //     region.editing_position_3d.z as i32,
96            // );
97
98            // println!("{} {:?}", height, h);
99
100            let position = region.editing_position_3d + Vec3::new(0.0, height, 0.0);
101
102            rusterix
103                .client
104                .camera_d3
105                .set_parameter_vec3("position", position);
106            let center = region.editing_look_at_3d + Vec3::new(0.0, 1.5, 0.0);
107            rusterix
108                .client
109                .camera_d3
110                .set_parameter_vec3("center", center);
111        } else if server_ctx.editor_view_mode == EditorViewMode::Iso {
112            rusterix.client.camera_d3 = Box::new(self.iso_camera.clone());
113
114            rusterix.client.camera_d3.set_parameter_f32(
115                "azimuth_deg",
116                self.iso_camera.get_parameter_f32("azimuth_deg"),
117            );
118
119            rusterix.client.camera_d3.set_parameter_f32(
120                "elevation_deg",
121                self.iso_camera.get_parameter_f32("elevation_deg"),
122            );
123
124            rusterix
125                .client
126                .camera_d3
127                .set_parameter_vec3("center", region.editing_position_3d);
128            rusterix.client.camera_d3.set_parameter_vec3(
129                "position",
130                region.editing_position_3d + vek::Vec3::new(-20.0, 20.0, 20.0),
131            );
132        } else if server_ctx.editor_view_mode == EditorViewMode::Orbit {
133            rusterix.client.camera_d3 = Box::new(self.orbit_camera.clone());
134
135            rusterix
136                .client
137                .camera_d3
138                .set_parameter_vec3("center", region.editing_position_3d);
139        }
140    }
141
142    /// Update move actions
143    pub fn update_action(&mut self, region: &mut Region, server_ctx: &mut ServerContext) {
144        let speed = 0.2;
145        let yaw_step = 4.0;
146        if server_ctx.editor_view_mode == EditorViewMode::FirstP {
147            match &self.move_action {
148                Some(CustomMoveAction::Forward) => {
149                    let (mut np, mut nl) = self.move_camera(
150                        region.editing_position_3d,
151                        region.editing_look_at_3d,
152                        Vec3::new(0.0, 0.0, 1.0),
153                        speed,
154                    );
155                    np.y = region.map.terrain.sample_height_bilinear(np.x, np.z) + 0.5;
156                    nl.y = np.y;
157                    region.editing_position_3d = np;
158                    region.editing_look_at_3d = nl;
159                }
160                Some(CustomMoveAction::Backward) => {
161                    let (mut np, mut nl) = self.move_camera(
162                        region.editing_position_3d,
163                        region.editing_look_at_3d,
164                        Vec3::new(0.0, 0.0, -1.0),
165                        speed,
166                    );
167                    np.y = region.map.terrain.sample_height_bilinear(np.x, np.z) + 0.5;
168                    nl.y = np.y;
169                    region.editing_position_3d = np;
170                    region.editing_look_at_3d = nl;
171                }
172                Some(CustomMoveAction::Left) => {
173                    let nl = self.rotate_camera_y(
174                        region.editing_position_3d,
175                        region.editing_look_at_3d,
176                        yaw_step,
177                    );
178                    region.editing_look_at_3d = nl;
179                }
180                Some(CustomMoveAction::Right) => {
181                    let nl = self.rotate_camera_y(
182                        region.editing_position_3d,
183                        region.editing_look_at_3d,
184                        -yaw_step,
185                    );
186                    region.editing_look_at_3d = nl;
187                }
188                None => {}
189            }
190        }
191    }
192
193    pub fn mouse_dragged(
194        &mut self,
195        region: &mut Region,
196        server_ctx: &mut ServerContext,
197        coord: &Vec2<i32>,
198    ) {
199        if server_ctx.editor_view_mode == EditorViewMode::FirstP {
200            let sens_yaw = 0.15; // deg per pixel horizontally
201            let sens_pitch = 0.15; // deg per pixel vertically
202            let max_pitch = 85.0; // don’t let camera flip
203
204            let curr = *coord;
205
206            if let Some(prev) = self.last_mouse {
207                let dx = (curr.x - prev.x) as f32;
208                let dy = (curr.y - prev.y) as f32;
209
210                // Yaw   (left / right)
211                if dx.abs() > 0.0 {
212                    region.editing_look_at_3d = self.rotate_camera_y(
213                        region.editing_position_3d,
214                        region.editing_look_at_3d,
215                        -dx * sens_yaw, // screen → world: left = +yaw
216                    );
217                }
218                // Pitch (up / down)
219                if dy.abs() > 0.0 {
220                    let look = self.rotate_camera_pitch(
221                        region.editing_position_3d,
222                        region.editing_look_at_3d,
223                        -dy * sens_pitch, // screen up = pitch up
224                    );
225                    region.editing_look_at_3d =
226                        self.clamp_pitch(region.editing_position_3d, look, max_pitch);
227                }
228            }
229            self.last_mouse = Some(curr);
230        }
231    }
232
233    fn camera_axes(&self, pos: Vec3<f32>, look_at: Vec3<f32>) -> (Vec3<f32>, Vec3<f32>, Vec3<f32>) {
234        let forward = (look_at - pos).normalized();
235        let world_up = Vec3::unit_y();
236        let right = forward.cross(world_up).normalized();
237        let up = right.cross(forward);
238        (forward, right, up)
239    }
240
241    fn move_camera(
242        &self,
243        mut pos: Vec3<f32>,
244        mut look_at: Vec3<f32>,
245        dir: Vec3<f32>, // e.g. (0,0,1) for “W”, (1,0,0) for “D” …
246        speed: f32,
247    ) -> (Vec3<f32>, Vec3<f32>) {
248        let (fwd, right, up) = self.camera_axes(pos, look_at);
249        let world_move = right * dir.x + up * dir.y + fwd * dir.z;
250        let world_move = world_move * speed;
251        pos += world_move;
252        look_at += world_move;
253        (pos, look_at)
254    }
255
256    pub fn rotate_camera_y(&self, pos: Vec3<f32>, look_at: Vec3<f32>, yaw_deg: f32) -> Vec3<f32> {
257        let dir = look_at - pos; // current forward
258        let r = yaw_deg.to_radians();
259        let (s, c) = r.sin_cos();
260        let new_dir = Vec3::new(dir.x * c + dir.z * s, dir.y, -dir.x * s + dir.z * c);
261        pos + new_dir
262    }
263
264    pub fn rotate_camera_pitch(
265        &self,
266        pos: Vec3<f32>,
267        look_at: Vec3<f32>,
268        pitch_deg: f32,
269    ) -> Vec3<f32> {
270        let dir = look_at - pos; // current forward
271        let len = dir.magnitude();
272        if len == 0.0 {
273            return look_at; // degeneracy guard
274        }
275
276        let forward = dir / len;
277        let right = forward.cross(Vec3::unit_y()).normalized();
278
279        let r = pitch_deg.to_radians();
280        let (s, c) = r.sin_cos();
281
282        let new_fwd =
283            forward * c + right.cross(forward) * s + right * right.dot(forward) * (1.0 - c);
284
285        pos + new_fwd * len // same distance, new dir
286    }
287
288    pub fn clamp_pitch(&self, old_pos: Vec3<f32>, new_look: Vec3<f32>, max_deg: f32) -> Vec3<f32> {
289        let dir = (new_look - old_pos).normalized();
290        let pitch = dir.y.asin().to_degrees(); // +90 top, -90 bottom
291        let clamped = pitch.clamp(-max_deg, max_deg);
292
293        if (pitch - clamped).abs() < 0.0001 {
294            new_look
295        } else {
296            self.rotate_camera_pitch(old_pos, new_look, clamped - pitch)
297        }
298    }
299
300    pub fn scroll_by(&mut self, coord: f32, server_ctx: &mut ServerContext) {
301        if server_ctx.editor_view_mode == EditorViewMode::Iso {
302            self.iso_camera.zoom(coord);
303        } else if server_ctx.editor_view_mode == EditorViewMode::Orbit {
304            self.orbit_camera.zoom(coord);
305        } else if server_ctx.editor_view_mode == EditorViewMode::FirstP {
306            self.firstp_camera.zoom(coord);
307        }
308    }
309
310    pub fn rotate(&mut self, delta: Vec2<f32>, server_ctx: &mut ServerContext) {
311        if server_ctx.editor_view_mode == EditorViewMode::Orbit {
312            self.orbit_camera.rotate(delta);
313        }
314    }
315}