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 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 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 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; let sens_pitch = 0.15; let max_pitch = 85.0; 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 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, );
217 }
218 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, );
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>, 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; 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; let len = dir.magnitude();
272 if len == 0.0 {
273 return look_at; }
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 }
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(); 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}