1#[cfg(target_arch = "wasm32")]
6use crate::storage::save_camera;
7use crate::storage::CameraStorage;
8use bevy::ecs::message::MessageReader;
9use bevy::input::mouse::{MouseMotion, MouseWheel};
10use bevy::input::touch::Touches;
11use bevy::prelude::*;
12
13#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
15pub struct CameraInputSet;
16
17pub struct CameraPlugin;
19
20impl Plugin for CameraPlugin {
21 fn build(&self, app: &mut App) {
22 app.init_resource::<CameraController>()
23 .add_systems(Startup, setup_camera)
24 .add_systems(
25 Update,
26 (
27 poll_camera_commands_system,
28 camera_input_system,
29 camera_touch_system,
30 camera_update_system,
31 camera_keyboard_system,
32 )
33 .chain()
34 .in_set(CameraInputSet),
35 );
36 }
37}
38
39impl CameraPlugin {
40 pub fn input_system_set() -> CameraInputSet {
42 CameraInputSet
43 }
44}
45
46#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
48pub enum CameraMode {
49 #[default]
50 Orbit,
51 Pan,
52 Walk,
53}
54
55#[derive(Resource)]
57pub struct CameraController {
58 pub mode: CameraMode,
60 pub target: Vec3,
62 pub distance: f32,
64 pub azimuth: f32,
66 pub elevation: f32,
68 pub damping: f32,
70 pub velocity: Vec3,
72 pub angular_velocity: Vec2,
74 pub is_animating: bool,
76 pub animation_target: Option<CameraAnimationTarget>,
78 pub fov: f32,
80 pub near: f32,
82 pub far: f32,
84 pub walk_speed: f32,
86 pub orbit_sensitivity: f32,
88 pub pan_sensitivity: f32,
90 pub zoom_sensitivity: f32,
92 pub is_dragging: bool,
94 pub last_mouse_pos: Vec2,
96 pub drag_start_pos: Vec2,
98 pub did_drag: bool,
100 pub just_clicked: bool,
102 pub touch_count: usize,
105 pub last_touch_pos: Vec2,
107 pub last_pinch_distance: f32,
109 pub last_two_touch_center: Vec2,
111 pub is_touch_dragging: bool,
113 pub touch_drag_start: Vec2,
115 pub touch_did_drag: bool,
117}
118
119impl Default for CameraController {
120 fn default() -> Self {
121 Self {
122 mode: CameraMode::Orbit,
123 target: Vec3::ZERO,
124 distance: 100.0, azimuth: 0.785, elevation: 0.615, damping: 0.92,
128 velocity: Vec3::ZERO,
129 angular_velocity: Vec2::ZERO,
130 is_animating: false,
131 animation_target: None,
132 fov: 45.0,
133 near: 0.05, far: 10000.0, walk_speed: 500.0, orbit_sensitivity: 0.005,
137 pan_sensitivity: 0.01,
138 zoom_sensitivity: 0.02,
139 is_dragging: false,
140 last_mouse_pos: Vec2::ZERO,
141 drag_start_pos: Vec2::ZERO,
142 did_drag: false,
143 just_clicked: false,
144 touch_count: 0,
145 last_touch_pos: Vec2::ZERO,
146 last_pinch_distance: 0.0,
147 last_two_touch_center: Vec2::ZERO,
148 is_touch_dragging: false,
149 touch_drag_start: Vec2::ZERO,
150 touch_did_drag: false,
151 }
152 }
153}
154
155impl CameraController {
156 pub fn get_position(&self) -> Vec3 {
158 let x = self.distance * self.elevation.cos() * self.azimuth.sin();
159 let y = self.distance * self.elevation.sin();
160 let z = self.distance * self.elevation.cos() * self.azimuth.cos();
161 self.target + Vec3::new(x, y, z)
162 }
163
164 pub fn set_preset_view(&mut self, azimuth: f32, elevation: f32) {
166 self.animation_target = Some(CameraAnimationTarget {
167 azimuth,
168 elevation,
169 distance: self.distance,
170 target: self.target,
171 duration: 0.5,
172 elapsed: 0.0,
173 });
174 self.is_animating = true;
175 }
176
177 pub fn home(&mut self) {
179 self.set_preset_view(0.785, 0.615); }
181
182 pub fn fit_bounds(&mut self, min: Vec3, max: Vec3) {
184 let center = (min + max) * 0.5;
185 let size = max - min;
186 let diagonal = size.length();
187
188 let fov_rad = self.fov.to_radians();
190 let distance = diagonal / (2.0 * (fov_rad / 2.0).tan());
191
192 self.animation_target = Some(CameraAnimationTarget {
193 azimuth: self.azimuth,
194 elevation: self.elevation,
195 distance: distance.max(1.0),
196 target: center,
197 duration: 0.5,
198 elapsed: 0.0,
199 });
200 self.is_animating = true;
201 }
202
203 pub fn frame(&mut self, min: Vec3, max: Vec3) {
205 self.fit_bounds(min, max);
206 }
207
208 pub fn zoom_in(&mut self) {
210 self.distance = (self.distance * 0.8).max(1.0);
211 }
212
213 pub fn zoom_out(&mut self) {
215 self.distance = (self.distance * 1.25).min(500000.0);
216 }
217
218 pub fn to_storage(&self) -> CameraStorage {
220 CameraStorage {
221 azimuth: self.azimuth,
222 elevation: self.elevation,
223 distance: self.distance,
224 target: [self.target.x, self.target.y, self.target.z],
225 }
226 }
227
228 pub fn from_storage(&mut self, storage: &CameraStorage) {
230 self.azimuth = storage.azimuth;
231 self.elevation = storage.elevation;
232 self.distance = storage.distance;
233 self.target = Vec3::new(storage.target[0], storage.target[1], storage.target[2]);
234 }
235}
236
237#[derive(Clone, Debug)]
239pub struct CameraAnimationTarget {
240 pub azimuth: f32,
241 pub elevation: f32,
242 pub distance: f32,
243 pub target: Vec3,
244 pub duration: f32,
245 pub elapsed: f32,
246}
247
248#[derive(Component)]
250pub struct MainCamera;
251
252#[derive(Component)]
255pub struct ArchitecturalLight;
256
257#[allow(unused_variables, unused_mut)]
259fn poll_camera_commands_system(
260 mut controller: ResMut<CameraController>,
261 scene_data: Res<crate::IfcSceneData>,
262) {
263 #[cfg(target_arch = "wasm32")]
264 {
265 if let Some(cmd) = crate::storage::load_camera_cmd() {
266 crate::storage::clear_camera_cmd();
267
268 match cmd.cmd.as_str() {
269 "home" => {
270 controller.home();
271 }
272 "fit_all" => {
273 if let Some(ref bounds) = scene_data.bounds {
274 controller.fit_bounds(bounds.min, bounds.max);
275 }
276 }
277 "set_mode" => {
278 if let Some(mode) = cmd.mode {
279 controller.mode = match mode.as_str() {
280 "pan" => CameraMode::Pan,
281 "walk" => CameraMode::Walk,
282 _ => CameraMode::Orbit,
283 };
284 }
285 }
286 _ => {}
287 }
288 }
289 }
290}
291
292fn setup_camera(mut commands: Commands, controller: Res<CameraController>) {
294 use bevy::core_pipeline::tonemapping::Tonemapping;
295 use bevy::render::view::Msaa;
296
297 let position = controller.get_position();
298
299 commands.spawn((
300 Camera3d::default(),
301 Transform::from_translation(position).looking_at(controller.target, Vec3::Y),
302 Projection::Perspective(PerspectiveProjection {
303 fov: controller.fov.to_radians(),
304 near: controller.near,
305 far: controller.far,
306 ..default()
307 }),
308 MainCamera,
309 Msaa::Sample4,
311 Tonemapping::AgX,
313 bevy::light::cluster::ClusterConfig::Single,
316 ));
317
318 commands.spawn(AmbientLight {
320 color: Color::srgb(0.9, 0.92, 1.0), brightness: 150.0,
322 affects_lightmapped_meshes: true,
323 });
324
325 commands.spawn((
327 DirectionalLight {
328 color: Color::srgb(1.0, 0.98, 0.95), illuminance: 30000.0,
330 shadows_enabled: false,
331 affects_lightmapped_mesh_diffuse: true,
332 ..default()
333 },
334 Transform::from_xyz(0.5, 1.0, 0.3).looking_at(Vec3::ZERO, Vec3::Y),
335 ArchitecturalLight,
336 ));
337
338 commands.spawn((
340 DirectionalLight {
341 color: Color::srgb(0.8, 0.88, 1.0), illuminance: 12000.0,
343 shadows_enabled: false,
344 affects_lightmapped_mesh_diffuse: true,
345 ..default()
346 },
347 Transform::from_xyz(-0.5, 0.3, -0.5).looking_at(Vec3::ZERO, Vec3::Y),
348 ArchitecturalLight,
349 ));
350
351 commands.spawn((
353 DirectionalLight {
354 color: Color::srgb(0.95, 0.95, 1.0),
355 illuminance: 8000.0,
356 shadows_enabled: false,
357 affects_lightmapped_mesh_diffuse: true,
358 ..default()
359 },
360 Transform::from_xyz(-0.3, 0.8, -0.8).looking_at(Vec3::ZERO, Vec3::Y),
361 ArchitecturalLight,
362 ));
363
364 commands.spawn((
366 DirectionalLight {
367 color: Color::srgb(0.7, 0.75, 0.85),
368 illuminance: 3000.0,
369 shadows_enabled: false,
370 affects_lightmapped_mesh_diffuse: true,
371 ..default()
372 },
373 Transform::from_xyz(0.0, -1.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
374 ArchitecturalLight,
375 ));
376}
377
378#[allow(unused_variables)]
380fn camera_input_system(
381 mouse_button: Res<ButtonInput<MouseButton>>,
382 mut mouse_motion: MessageReader<MouseMotion>,
383 mut mouse_wheel: MessageReader<MouseWheel>,
384 mut controller: ResMut<CameraController>,
385 windows: Query<&Window>,
386 measure_state: Res<crate::picking::MeasurementState>,
387 #[cfg(feature = "bevy-ui")] ui_interactions: Query<&Interaction, With<Node>>,
389) {
390 let Ok(window) = windows.single() else { return };
391
392 #[cfg(feature = "bevy-ui")]
394 let mouse_over_ui = ui_interactions
395 .iter()
396 .any(|interaction| matches!(interaction, Interaction::Hovered | Interaction::Pressed));
397 #[cfg(not(feature = "bevy-ui"))]
398 let mouse_over_ui = false;
399
400 let is_measure = measure_state.active;
402
403 if mouse_button.just_pressed(MouseButton::Left) && !mouse_over_ui && !is_measure {
405 controller.is_dragging = true;
406 controller.did_drag = false;
407 controller.just_clicked = false; if let Some(pos) = window.cursor_position() {
409 controller.last_mouse_pos = pos;
410 controller.drag_start_pos = pos;
411 }
412 }
413 if mouse_button.just_released(MouseButton::Left) {
414 if !controller.did_drag {
416 controller.just_clicked = true;
417 }
418 controller.is_dragging = false;
419 }
420 if is_measure && mouse_button.just_pressed(MouseButton::Left) && !mouse_over_ui {
422 controller.just_clicked = true;
423 if let Some(pos) = window.cursor_position() {
424 controller.drag_start_pos = pos;
425 }
426 }
427
428 if controller.is_dragging {
430 for ev in mouse_motion.read() {
431 if ev.delta.length() > 3.0 {
433 controller.did_drag = true;
434 }
435
436 match controller.mode {
437 CameraMode::Orbit => {
438 controller.azimuth -= ev.delta.x * controller.orbit_sensitivity;
439 controller.elevation -= ev.delta.y * controller.orbit_sensitivity;
440 controller.elevation = controller.elevation.clamp(-1.5, 1.5);
442 controller.angular_velocity = ev.delta * controller.orbit_sensitivity;
444 }
445 CameraMode::Pan => {
446 let right = Vec3::new(controller.azimuth.cos(), 0.0, -controller.azimuth.sin());
448 let up = Vec3::Y;
449 let pan = right
450 * ev.delta.x
451 * controller.pan_sensitivity
452 * controller.distance
453 * 0.01
454 - up * ev.delta.y * controller.pan_sensitivity * controller.distance * 0.01;
455 controller.target += pan;
456 }
457 CameraMode::Walk => {
458 controller.azimuth -= ev.delta.x * controller.orbit_sensitivity * 0.5;
460 controller.elevation -= ev.delta.y * controller.orbit_sensitivity * 0.5;
461 controller.elevation = controller.elevation.clamp(-1.5, 1.5);
462 }
463 }
464 }
465 } else {
466 let damping = controller.damping;
468 controller.angular_velocity *= damping;
469 if controller.angular_velocity.length() > 0.0001 {
470 controller.azimuth -= controller.angular_velocity.x;
471 controller.elevation -= controller.angular_velocity.y;
472 controller.elevation = controller.elevation.clamp(-1.5, 1.5);
473 }
474 }
475
476 if !mouse_over_ui {
478 for ev in mouse_wheel.read() {
479 let zoom_delta = ev.y * controller.zoom_sensitivity;
480 controller.distance = (controller.distance * (1.0 - zoom_delta)).clamp(1.0, 500000.0);
481 }
482 }
483}
484
485fn camera_touch_system(touches: Res<Touches>, mut controller: ResMut<CameraController>) {
493 let pressed: Vec<Vec2> = touches.iter().map(|t| t.position()).collect();
494 let count = pressed.len();
495 let prev_count = controller.touch_count;
496
497 if count == 1 {
499 let pos = pressed[0];
500
501 if prev_count == 0 {
502 controller.is_touch_dragging = true;
504 controller.touch_did_drag = false;
505 controller.last_touch_pos = pos;
506 controller.touch_drag_start = pos;
507 } else if controller.is_touch_dragging && prev_count == 1 {
508 let delta = pos - controller.last_touch_pos;
510
511 if delta.length() > 2.0 {
512 controller.touch_did_drag = true;
513 }
514
515 if controller.touch_did_drag {
516 controller.azimuth -= delta.x * controller.orbit_sensitivity;
518 controller.elevation -= delta.y * controller.orbit_sensitivity;
519 controller.elevation = controller.elevation.clamp(-1.5, 1.5);
520 controller.angular_velocity = delta * controller.orbit_sensitivity;
521 }
522
523 controller.last_touch_pos = pos;
524 }
525 }
526
527 if count == 2 {
529 let center = (pressed[0] + pressed[1]) * 0.5;
530 let distance = (pressed[0] - pressed[1]).length();
531
532 if prev_count < 2 {
533 controller.last_two_touch_center = center;
535 controller.last_pinch_distance = distance;
536 controller.is_touch_dragging = false;
538 controller.touch_did_drag = true; } else {
540 let center_delta = center - controller.last_two_touch_center;
542 let right = Vec3::new(controller.azimuth.cos(), 0.0, -controller.azimuth.sin());
543 let up = Vec3::Y;
544 let pan =
545 right * center_delta.x * controller.pan_sensitivity * controller.distance * 0.01
546 - up * center_delta.y * controller.pan_sensitivity * controller.distance * 0.01;
547 controller.target += pan;
548
549 if controller.last_pinch_distance > 10.0 {
551 let zoom_ratio = distance / controller.last_pinch_distance;
552 controller.distance = (controller.distance / zoom_ratio).clamp(1.0, 500000.0);
553 }
554
555 controller.last_two_touch_center = center;
556 controller.last_pinch_distance = distance;
557 }
558 }
559
560 if count == 0 && prev_count > 0 {
562 if controller.is_touch_dragging && !controller.touch_did_drag {
563 controller.just_clicked = true;
565 controller.drag_start_pos = controller.touch_drag_start;
566 }
567 controller.is_touch_dragging = false;
568 }
569
570 controller.touch_count = count;
571}
572
573fn camera_keyboard_system(
575 keyboard: Res<ButtonInput<KeyCode>>,
576 mut controller: ResMut<CameraController>,
577 time: Res<Time>,
578) {
579 let dt = time.delta_secs();
580
581 if controller.mode == CameraMode::Walk {
583 let forward = Vec3::new(
584 -controller.azimuth.sin() * controller.elevation.cos(),
585 controller.elevation.sin(),
586 -controller.azimuth.cos() * controller.elevation.cos(),
587 )
588 .normalize();
589 let right = Vec3::new(controller.azimuth.cos(), 0.0, -controller.azimuth.sin());
590
591 let mut movement = Vec3::ZERO;
592
593 if keyboard.pressed(KeyCode::KeyW) || keyboard.pressed(KeyCode::ArrowUp) {
594 movement += forward;
595 }
596 if keyboard.pressed(KeyCode::KeyS) || keyboard.pressed(KeyCode::ArrowDown) {
597 movement -= forward;
598 }
599 if keyboard.pressed(KeyCode::KeyA) || keyboard.pressed(KeyCode::ArrowLeft) {
600 movement -= right;
601 }
602 if keyboard.pressed(KeyCode::KeyD) || keyboard.pressed(KeyCode::ArrowRight) {
603 movement += right;
604 }
605 if keyboard.pressed(KeyCode::KeyQ) {
606 movement -= Vec3::Y;
607 }
608 if keyboard.pressed(KeyCode::KeyE) {
609 movement += Vec3::Y;
610 }
611
612 if movement.length() > 0.0 {
613 let walk_speed = controller.walk_speed;
614 controller.target += movement.normalize() * walk_speed * dt;
615 }
616 }
617
618 if keyboard.just_pressed(KeyCode::Digit1) {
620 controller.set_preset_view(0.0, 0.0); }
622 if keyboard.just_pressed(KeyCode::Digit2) {
623 controller.set_preset_view(std::f32::consts::PI, 0.0); }
625 if keyboard.just_pressed(KeyCode::Digit3) {
626 controller.set_preset_view(-std::f32::consts::FRAC_PI_2, 0.0); }
628 if keyboard.just_pressed(KeyCode::Digit4) {
629 controller.set_preset_view(std::f32::consts::FRAC_PI_2, 0.0); }
631 if keyboard.just_pressed(KeyCode::Digit5) {
632 controller.set_preset_view(0.0, std::f32::consts::FRAC_PI_2 - 0.001); }
634 if keyboard.just_pressed(KeyCode::Digit6) {
635 controller.set_preset_view(0.0, -std::f32::consts::FRAC_PI_2 + 0.001); }
637 if keyboard.just_pressed(KeyCode::KeyH) {
638 controller.home(); }
640}
641
642fn camera_update_system(
644 mut controller: ResMut<CameraController>,
645 mut camera: Query<&mut Transform, With<MainCamera>>,
646 time: Res<Time>,
647) {
648 let dt = time.delta_secs();
649
650 if controller.animation_target.is_some() {
652 let animation_data = {
654 let target = controller.animation_target.as_mut().unwrap();
655 target.elapsed += dt;
656 let t = (target.elapsed / target.duration).min(1.0);
657 let t = 1.0 - (1.0 - t).powi(3);
659 let completed = target.elapsed >= target.duration;
660 (
661 target.azimuth,
662 target.elevation,
663 target.distance,
664 target.target,
665 t,
666 completed,
667 )
668 };
669
670 let (target_azimuth, target_elevation, target_distance, target_pos, t, completed) =
671 animation_data;
672
673 controller.azimuth = lerp(controller.azimuth, target_azimuth, t);
674 controller.elevation = lerp(controller.elevation, target_elevation, t);
675 controller.distance = lerp(controller.distance, target_distance, t);
676 controller.target = controller.target.lerp(target_pos, t);
677
678 if completed {
679 controller.animation_target = None;
680 controller.is_animating = false;
681 }
682 }
683
684 if let Ok(mut transform) = camera.single_mut() {
686 let position = controller.get_position();
687
688 transform.translation = transform
690 .translation
691 .lerp(position, 1.0 - controller.damping.powi(2));
692 transform.look_at(controller.target, Vec3::Y);
693 }
694
695 #[cfg(target_arch = "wasm32")]
697 {
698 static mut SAVE_COUNTER: u32 = 0;
700 unsafe {
701 SAVE_COUNTER += 1;
702 if SAVE_COUNTER % 30 == 0 {
703 save_camera(&controller.to_storage());
704 }
705 }
706 }
707}
708
709fn lerp(a: f32, b: f32, t: f32) -> f32 {
711 a + (b - a) * t
712}