#[cfg(target_arch = "wasm32")]
use crate::storage::save_camera;
use crate::storage::CameraStorage;
use bevy::ecs::message::MessageReader;
use bevy::input::mouse::{MouseMotion, MouseWheel};
use bevy::input::touch::Touches;
use bevy::prelude::*;
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct CameraInputSet;
pub struct CameraPlugin;
impl Plugin for CameraPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<CameraController>()
.add_systems(Startup, setup_camera)
.add_systems(
Update,
(
poll_camera_commands_system,
camera_input_system,
camera_touch_system,
camera_update_system,
camera_keyboard_system,
)
.chain()
.in_set(CameraInputSet),
);
}
}
impl CameraPlugin {
pub fn input_system_set() -> CameraInputSet {
CameraInputSet
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum CameraMode {
#[default]
Orbit,
Pan,
Walk,
}
#[derive(Resource)]
pub struct CameraController {
pub mode: CameraMode,
pub target: Vec3,
pub distance: f32,
pub azimuth: f32,
pub elevation: f32,
pub damping: f32,
pub velocity: Vec3,
pub angular_velocity: Vec2,
pub is_animating: bool,
pub animation_target: Option<CameraAnimationTarget>,
pub fov: f32,
pub near: f32,
pub far: f32,
pub walk_speed: f32,
pub orbit_sensitivity: f32,
pub pan_sensitivity: f32,
pub zoom_sensitivity: f32,
pub is_dragging: bool,
pub last_mouse_pos: Vec2,
pub drag_start_pos: Vec2,
pub did_drag: bool,
pub just_clicked: bool,
pub touch_count: usize,
pub last_touch_pos: Vec2,
pub last_pinch_distance: f32,
pub last_two_touch_center: Vec2,
pub is_touch_dragging: bool,
pub touch_drag_start: Vec2,
pub touch_did_drag: bool,
}
impl Default for CameraController {
fn default() -> Self {
Self {
mode: CameraMode::Orbit,
target: Vec3::ZERO,
distance: 100.0, azimuth: 0.785, elevation: 0.615, damping: 0.92,
velocity: Vec3::ZERO,
angular_velocity: Vec2::ZERO,
is_animating: false,
animation_target: None,
fov: 45.0,
near: 0.05, far: 10000.0, walk_speed: 500.0, orbit_sensitivity: 0.005,
pan_sensitivity: 0.01,
zoom_sensitivity: 0.02,
is_dragging: false,
last_mouse_pos: Vec2::ZERO,
drag_start_pos: Vec2::ZERO,
did_drag: false,
just_clicked: false,
touch_count: 0,
last_touch_pos: Vec2::ZERO,
last_pinch_distance: 0.0,
last_two_touch_center: Vec2::ZERO,
is_touch_dragging: false,
touch_drag_start: Vec2::ZERO,
touch_did_drag: false,
}
}
}
impl CameraController {
pub fn get_position(&self) -> Vec3 {
let x = self.distance * self.elevation.cos() * self.azimuth.sin();
let y = self.distance * self.elevation.sin();
let z = self.distance * self.elevation.cos() * self.azimuth.cos();
self.target + Vec3::new(x, y, z)
}
pub fn set_preset_view(&mut self, azimuth: f32, elevation: f32) {
self.animation_target = Some(CameraAnimationTarget {
azimuth,
elevation,
distance: self.distance,
target: self.target,
duration: 0.5,
elapsed: 0.0,
});
self.is_animating = true;
}
pub fn home(&mut self) {
self.set_preset_view(0.785, 0.615); }
pub fn fit_bounds(&mut self, min: Vec3, max: Vec3) {
let center = (min + max) * 0.5;
let size = max - min;
let diagonal = size.length();
let fov_rad = self.fov.to_radians();
let distance = diagonal / (2.0 * (fov_rad / 2.0).tan());
self.animation_target = Some(CameraAnimationTarget {
azimuth: self.azimuth,
elevation: self.elevation,
distance: distance.max(1.0),
target: center,
duration: 0.5,
elapsed: 0.0,
});
self.is_animating = true;
}
pub fn frame(&mut self, min: Vec3, max: Vec3) {
self.fit_bounds(min, max);
}
pub fn zoom_in(&mut self) {
self.distance = (self.distance * 0.8).max(1.0);
}
pub fn zoom_out(&mut self) {
self.distance = (self.distance * 1.25).min(500000.0);
}
pub fn to_storage(&self) -> CameraStorage {
CameraStorage {
azimuth: self.azimuth,
elevation: self.elevation,
distance: self.distance,
target: [self.target.x, self.target.y, self.target.z],
}
}
pub fn from_storage(&mut self, storage: &CameraStorage) {
self.azimuth = storage.azimuth;
self.elevation = storage.elevation;
self.distance = storage.distance;
self.target = Vec3::new(storage.target[0], storage.target[1], storage.target[2]);
}
}
#[derive(Clone, Debug)]
pub struct CameraAnimationTarget {
pub azimuth: f32,
pub elevation: f32,
pub distance: f32,
pub target: Vec3,
pub duration: f32,
pub elapsed: f32,
}
#[derive(Component)]
pub struct MainCamera;
#[derive(Component)]
pub struct ArchitecturalLight;
#[allow(unused_variables, unused_mut)]
fn poll_camera_commands_system(
mut controller: ResMut<CameraController>,
scene_data: Res<crate::IfcSceneData>,
) {
#[cfg(target_arch = "wasm32")]
{
if let Some(cmd) = crate::storage::load_camera_cmd() {
crate::storage::clear_camera_cmd();
match cmd.cmd.as_str() {
"home" => {
controller.home();
}
"fit_all" => {
if let Some(ref bounds) = scene_data.bounds {
controller.fit_bounds(bounds.min, bounds.max);
}
}
"set_mode" => {
if let Some(mode) = cmd.mode {
controller.mode = match mode.as_str() {
"pan" => CameraMode::Pan,
"walk" => CameraMode::Walk,
_ => CameraMode::Orbit,
};
}
}
_ => {}
}
}
}
}
fn setup_camera(mut commands: Commands, controller: Res<CameraController>) {
use bevy::core_pipeline::tonemapping::Tonemapping;
use bevy::render::view::Msaa;
let position = controller.get_position();
commands.spawn((
Camera3d::default(),
Transform::from_translation(position).looking_at(controller.target, Vec3::Y),
Projection::Perspective(PerspectiveProjection {
fov: controller.fov.to_radians(),
near: controller.near,
far: controller.far,
..default()
}),
MainCamera,
Msaa::Sample4,
Tonemapping::AgX,
bevy::light::cluster::ClusterConfig::Single,
));
commands.spawn(AmbientLight {
color: Color::srgb(0.9, 0.92, 1.0), brightness: 150.0,
affects_lightmapped_meshes: true,
});
commands.spawn((
DirectionalLight {
color: Color::srgb(1.0, 0.98, 0.95), illuminance: 30000.0,
shadows_enabled: false,
affects_lightmapped_mesh_diffuse: true,
..default()
},
Transform::from_xyz(0.5, 1.0, 0.3).looking_at(Vec3::ZERO, Vec3::Y),
ArchitecturalLight,
));
commands.spawn((
DirectionalLight {
color: Color::srgb(0.8, 0.88, 1.0), illuminance: 12000.0,
shadows_enabled: false,
affects_lightmapped_mesh_diffuse: true,
..default()
},
Transform::from_xyz(-0.5, 0.3, -0.5).looking_at(Vec3::ZERO, Vec3::Y),
ArchitecturalLight,
));
commands.spawn((
DirectionalLight {
color: Color::srgb(0.95, 0.95, 1.0),
illuminance: 8000.0,
shadows_enabled: false,
affects_lightmapped_mesh_diffuse: true,
..default()
},
Transform::from_xyz(-0.3, 0.8, -0.8).looking_at(Vec3::ZERO, Vec3::Y),
ArchitecturalLight,
));
commands.spawn((
DirectionalLight {
color: Color::srgb(0.7, 0.75, 0.85),
illuminance: 3000.0,
shadows_enabled: false,
affects_lightmapped_mesh_diffuse: true,
..default()
},
Transform::from_xyz(0.0, -1.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
ArchitecturalLight,
));
}
#[allow(unused_variables)]
fn camera_input_system(
mouse_button: Res<ButtonInput<MouseButton>>,
mut mouse_motion: MessageReader<MouseMotion>,
mut mouse_wheel: MessageReader<MouseWheel>,
mut controller: ResMut<CameraController>,
windows: Query<&Window>,
measure_state: Res<crate::picking::MeasurementState>,
#[cfg(feature = "bevy-ui")] ui_interactions: Query<&Interaction, With<Node>>,
) {
let Ok(window) = windows.single() else { return };
#[cfg(feature = "bevy-ui")]
let mouse_over_ui = ui_interactions
.iter()
.any(|interaction| matches!(interaction, Interaction::Hovered | Interaction::Pressed));
#[cfg(not(feature = "bevy-ui"))]
let mouse_over_ui = false;
let is_measure = measure_state.active;
if mouse_button.just_pressed(MouseButton::Left) && !mouse_over_ui && !is_measure {
controller.is_dragging = true;
controller.did_drag = false;
controller.just_clicked = false; if let Some(pos) = window.cursor_position() {
controller.last_mouse_pos = pos;
controller.drag_start_pos = pos;
}
}
if mouse_button.just_released(MouseButton::Left) {
if !controller.did_drag {
controller.just_clicked = true;
}
controller.is_dragging = false;
}
if is_measure && mouse_button.just_pressed(MouseButton::Left) && !mouse_over_ui {
controller.just_clicked = true;
if let Some(pos) = window.cursor_position() {
controller.drag_start_pos = pos;
}
}
if controller.is_dragging {
for ev in mouse_motion.read() {
if ev.delta.length() > 3.0 {
controller.did_drag = true;
}
match controller.mode {
CameraMode::Orbit => {
controller.azimuth -= ev.delta.x * controller.orbit_sensitivity;
controller.elevation -= ev.delta.y * controller.orbit_sensitivity;
controller.elevation = controller.elevation.clamp(-1.5, 1.5);
controller.angular_velocity = ev.delta * controller.orbit_sensitivity;
}
CameraMode::Pan => {
let right = Vec3::new(controller.azimuth.cos(), 0.0, -controller.azimuth.sin());
let up = Vec3::Y;
let pan = right
* ev.delta.x
* controller.pan_sensitivity
* controller.distance
* 0.01
- up * ev.delta.y * controller.pan_sensitivity * controller.distance * 0.01;
controller.target += pan;
}
CameraMode::Walk => {
controller.azimuth -= ev.delta.x * controller.orbit_sensitivity * 0.5;
controller.elevation -= ev.delta.y * controller.orbit_sensitivity * 0.5;
controller.elevation = controller.elevation.clamp(-1.5, 1.5);
}
}
}
} else {
let damping = controller.damping;
controller.angular_velocity *= damping;
if controller.angular_velocity.length() > 0.0001 {
controller.azimuth -= controller.angular_velocity.x;
controller.elevation -= controller.angular_velocity.y;
controller.elevation = controller.elevation.clamp(-1.5, 1.5);
}
}
if !mouse_over_ui {
for ev in mouse_wheel.read() {
let zoom_delta = ev.y * controller.zoom_sensitivity;
controller.distance = (controller.distance * (1.0 - zoom_delta)).clamp(1.0, 500000.0);
}
}
}
fn camera_touch_system(touches: Res<Touches>, mut controller: ResMut<CameraController>) {
let pressed: Vec<Vec2> = touches.iter().map(|t| t.position()).collect();
let count = pressed.len();
let prev_count = controller.touch_count;
if count == 1 {
let pos = pressed[0];
if prev_count == 0 {
controller.is_touch_dragging = true;
controller.touch_did_drag = false;
controller.last_touch_pos = pos;
controller.touch_drag_start = pos;
} else if controller.is_touch_dragging && prev_count == 1 {
let delta = pos - controller.last_touch_pos;
if delta.length() > 2.0 {
controller.touch_did_drag = true;
}
if controller.touch_did_drag {
controller.azimuth -= delta.x * controller.orbit_sensitivity;
controller.elevation -= delta.y * controller.orbit_sensitivity;
controller.elevation = controller.elevation.clamp(-1.5, 1.5);
controller.angular_velocity = delta * controller.orbit_sensitivity;
}
controller.last_touch_pos = pos;
}
}
if count == 2 {
let center = (pressed[0] + pressed[1]) * 0.5;
let distance = (pressed[0] - pressed[1]).length();
if prev_count < 2 {
controller.last_two_touch_center = center;
controller.last_pinch_distance = distance;
controller.is_touch_dragging = false;
controller.touch_did_drag = true; } else {
let center_delta = center - controller.last_two_touch_center;
let right = Vec3::new(controller.azimuth.cos(), 0.0, -controller.azimuth.sin());
let up = Vec3::Y;
let pan =
right * center_delta.x * controller.pan_sensitivity * controller.distance * 0.01
- up * center_delta.y * controller.pan_sensitivity * controller.distance * 0.01;
controller.target += pan;
if controller.last_pinch_distance > 10.0 {
let zoom_ratio = distance / controller.last_pinch_distance;
controller.distance = (controller.distance / zoom_ratio).clamp(1.0, 500000.0);
}
controller.last_two_touch_center = center;
controller.last_pinch_distance = distance;
}
}
if count == 0 && prev_count > 0 {
if controller.is_touch_dragging && !controller.touch_did_drag {
controller.just_clicked = true;
controller.drag_start_pos = controller.touch_drag_start;
}
controller.is_touch_dragging = false;
}
controller.touch_count = count;
}
fn camera_keyboard_system(
keyboard: Res<ButtonInput<KeyCode>>,
mut controller: ResMut<CameraController>,
time: Res<Time>,
) {
let dt = time.delta_secs();
if controller.mode == CameraMode::Walk {
let forward = Vec3::new(
-controller.azimuth.sin() * controller.elevation.cos(),
controller.elevation.sin(),
-controller.azimuth.cos() * controller.elevation.cos(),
)
.normalize();
let right = Vec3::new(controller.azimuth.cos(), 0.0, -controller.azimuth.sin());
let mut movement = Vec3::ZERO;
if keyboard.pressed(KeyCode::KeyW) || keyboard.pressed(KeyCode::ArrowUp) {
movement += forward;
}
if keyboard.pressed(KeyCode::KeyS) || keyboard.pressed(KeyCode::ArrowDown) {
movement -= forward;
}
if keyboard.pressed(KeyCode::KeyA) || keyboard.pressed(KeyCode::ArrowLeft) {
movement -= right;
}
if keyboard.pressed(KeyCode::KeyD) || keyboard.pressed(KeyCode::ArrowRight) {
movement += right;
}
if keyboard.pressed(KeyCode::KeyQ) {
movement -= Vec3::Y;
}
if keyboard.pressed(KeyCode::KeyE) {
movement += Vec3::Y;
}
if movement.length() > 0.0 {
let walk_speed = controller.walk_speed;
controller.target += movement.normalize() * walk_speed * dt;
}
}
if keyboard.just_pressed(KeyCode::Digit1) {
controller.set_preset_view(0.0, 0.0); }
if keyboard.just_pressed(KeyCode::Digit2) {
controller.set_preset_view(std::f32::consts::PI, 0.0); }
if keyboard.just_pressed(KeyCode::Digit3) {
controller.set_preset_view(-std::f32::consts::FRAC_PI_2, 0.0); }
if keyboard.just_pressed(KeyCode::Digit4) {
controller.set_preset_view(std::f32::consts::FRAC_PI_2, 0.0); }
if keyboard.just_pressed(KeyCode::Digit5) {
controller.set_preset_view(0.0, std::f32::consts::FRAC_PI_2 - 0.001); }
if keyboard.just_pressed(KeyCode::Digit6) {
controller.set_preset_view(0.0, -std::f32::consts::FRAC_PI_2 + 0.001); }
if keyboard.just_pressed(KeyCode::KeyH) {
controller.home(); }
}
fn camera_update_system(
mut controller: ResMut<CameraController>,
mut camera: Query<&mut Transform, With<MainCamera>>,
time: Res<Time>,
) {
let dt = time.delta_secs();
if controller.animation_target.is_some() {
let animation_data = {
let target = controller.animation_target.as_mut().unwrap();
target.elapsed += dt;
let t = (target.elapsed / target.duration).min(1.0);
let t = 1.0 - (1.0 - t).powi(3);
let completed = target.elapsed >= target.duration;
(
target.azimuth,
target.elevation,
target.distance,
target.target,
t,
completed,
)
};
let (target_azimuth, target_elevation, target_distance, target_pos, t, completed) =
animation_data;
controller.azimuth = lerp(controller.azimuth, target_azimuth, t);
controller.elevation = lerp(controller.elevation, target_elevation, t);
controller.distance = lerp(controller.distance, target_distance, t);
controller.target = controller.target.lerp(target_pos, t);
if completed {
controller.animation_target = None;
controller.is_animating = false;
}
}
if let Ok(mut transform) = camera.single_mut() {
let position = controller.get_position();
transform.translation = transform
.translation
.lerp(position, 1.0 - controller.damping.powi(2));
transform.look_at(controller.target, Vec3::Y);
}
#[cfg(target_arch = "wasm32")]
{
static mut SAVE_COUNTER: u32 = 0;
unsafe {
SAVE_COUNTER += 1;
if SAVE_COUNTER % 30 == 0 {
save_camera(&controller.to_storage());
}
}
}
}
fn lerp(a: f32, b: f32, t: f32) -> f32 {
a + (b - a) * t
}