244 lines
7.7 KiB
Rust
244 lines
7.7 KiB
Rust
use bevy::camera::visibility::RenderLayers;
|
|
use bevy::input::mouse::{AccumulatedMouseMotion, MouseWheel};
|
|
use bevy::prelude::*;
|
|
use bevy::window::{CursorOptions, PrimaryWindow};
|
|
use bevy_egui::{EguiContexts, EguiGlobalSettings, PrimaryEguiContext};
|
|
use transform_gizmo_bevy::prelude::GizmoCamera;
|
|
|
|
use crate::infra::EditorOnly;
|
|
use crate::state::scene_tools_active;
|
|
use crate::ui::{egui_captures_keyboard, UiState};
|
|
use settings::ProjectSettings;
|
|
|
|
#[derive(Component)]
|
|
pub struct EditorCamera {
|
|
pub fly_speed: f32,
|
|
pub look_sensitivity: f32,
|
|
pub pan_speed: f32,
|
|
}
|
|
|
|
impl Default for EditorCamera {
|
|
fn default() -> Self {
|
|
Self {
|
|
fly_speed: 14.0,
|
|
look_sensitivity: 0.003,
|
|
pan_speed: 0.015,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct EditorCameraPlugin;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
enum CameraNavigationMode {
|
|
Look,
|
|
Pan,
|
|
}
|
|
|
|
#[derive(Resource, Default)]
|
|
struct EditorCameraInputState {
|
|
mode: Option<CameraNavigationMode>,
|
|
cursor_hidden_for_look: bool,
|
|
cursor_visible_before_look: bool,
|
|
}
|
|
|
|
impl Plugin for EditorCameraPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.init_resource::<EditorCameraInputState>()
|
|
.add_systems(Startup, spawn_editor_camera)
|
|
.add_systems(
|
|
Update,
|
|
(sync_editor_camera_from_settings, update_editor_camera)
|
|
.chain()
|
|
.run_if(scene_tools_active),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn sync_editor_camera_from_settings(
|
|
settings: Res<ProjectSettings>,
|
|
mut cameras: Query<&mut EditorCamera>,
|
|
) {
|
|
if !settings.is_changed() {
|
|
return;
|
|
}
|
|
let input = &settings.input;
|
|
for mut camera in &mut cameras {
|
|
camera.fly_speed = input.editor_fly_speed;
|
|
camera.look_sensitivity = input.editor_look_sensitivity;
|
|
camera.pan_speed = input.editor_pan_speed;
|
|
}
|
|
}
|
|
|
|
fn spawn_editor_camera(
|
|
mut commands: Commands,
|
|
mut egui_global_settings: ResMut<EguiGlobalSettings>,
|
|
) {
|
|
// Own the egui primary context explicitly. If bevy_egui auto-attaches it to
|
|
// a 3D camera, `set_camera_viewport` then crops egui's own screen rect to the
|
|
// shrinking viewport region, and egui_dock eventually lays out into a NaN
|
|
// rect (panic: "rect is nan in advance_cursor_after_rect").
|
|
egui_global_settings.auto_create_primary_context = false;
|
|
|
|
commands.spawn((
|
|
Name::new("Editor Camera"),
|
|
EditorOnly,
|
|
EditorCamera::default(),
|
|
RenderLayers::default(),
|
|
GizmoCamera,
|
|
Camera3d::default(),
|
|
Camera {
|
|
clear_color: ClearColorConfig::Custom(Color::srgba(0.0, 0.0, 0.0, 1.0)),
|
|
..default()
|
|
},
|
|
Transform::from_xyz(10.0, 8.0, 18.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
));
|
|
|
|
// Dedicated full-window 2D camera that renders only egui on top of the 3D
|
|
// scene. Keeping the egui context off the 3D cameras avoids the
|
|
// viewport-cropping feedback loop described above.
|
|
commands.spawn((
|
|
Name::new("Editor Egui Camera"),
|
|
EditorOnly,
|
|
Camera2d,
|
|
PrimaryEguiContext,
|
|
RenderLayers::none(),
|
|
Camera {
|
|
order: 10,
|
|
clear_color: ClearColorConfig::None,
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn update_editor_camera(
|
|
time: Res<Time>,
|
|
keys: Res<ButtonInput<KeyCode>>,
|
|
buttons: Res<ButtonInput<MouseButton>>,
|
|
mouse_motion: Res<AccumulatedMouseMotion>,
|
|
mut scroll: MessageReader<MouseWheel>,
|
|
mut contexts: EguiContexts,
|
|
ui_state: Res<UiState>,
|
|
mut input_state: ResMut<EditorCameraInputState>,
|
|
mut cameras: Query<(&mut Transform, &EditorCamera)>,
|
|
mut cursor: Single<&mut CursorOptions, With<PrimaryWindow>>,
|
|
) -> Result {
|
|
let ctx = contexts.ctx_mut()?;
|
|
let in_scene = ui_state.pointer_in_viewport;
|
|
let navigation_pressed =
|
|
buttons.pressed(MouseButton::Right) || buttons.pressed(MouseButton::Middle);
|
|
if !navigation_pressed {
|
|
input_state.mode = None;
|
|
restore_editor_camera_cursor(&mut input_state, &mut cursor);
|
|
}
|
|
if egui_captures_keyboard(ctx) {
|
|
input_state.mode = None;
|
|
restore_editor_camera_cursor(&mut input_state, &mut cursor);
|
|
return Ok(());
|
|
}
|
|
if in_scene && buttons.just_pressed(MouseButton::Right) {
|
|
input_state.mode = Some(CameraNavigationMode::Look);
|
|
input_state.cursor_visible_before_look = cursor.visible;
|
|
input_state.cursor_hidden_for_look = true;
|
|
cursor.visible = false;
|
|
} else if in_scene && buttons.just_pressed(MouseButton::Middle) {
|
|
input_state.mode = Some(CameraNavigationMode::Pan);
|
|
restore_editor_camera_cursor(&mut input_state, &mut cursor);
|
|
}
|
|
if input_state.mode.is_none() && navigation_pressed {
|
|
return Ok(());
|
|
}
|
|
if !in_scene && input_state.mode.is_none() && ctx.wants_pointer_input() {
|
|
return Ok(());
|
|
}
|
|
if in_scene
|
|
&& input_state.mode.is_none()
|
|
&& ctx.wants_pointer_input()
|
|
&& !buttons.pressed(MouseButton::Right)
|
|
{
|
|
return Ok(());
|
|
}
|
|
|
|
let dt = time.delta_secs();
|
|
let raw_scroll_delta: f32 = scroll.read().map(|event| event.y).sum();
|
|
let scroll_delta = if in_scene && !ctx.wants_pointer_input() {
|
|
raw_scroll_delta
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
for (mut transform, camera) in &mut cameras {
|
|
let looking = matches!(input_state.mode, Some(CameraNavigationMode::Look))
|
|
&& buttons.pressed(MouseButton::Right);
|
|
let panning = matches!(input_state.mode, Some(CameraNavigationMode::Pan))
|
|
&& buttons.pressed(MouseButton::Middle);
|
|
|
|
if looking {
|
|
let delta = mouse_motion.delta;
|
|
if delta != Vec2::ZERO {
|
|
let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
|
|
let yaw = yaw - delta.x * camera.look_sensitivity;
|
|
let pitch = (pitch - delta.y * camera.look_sensitivity).clamp(
|
|
-std::f32::consts::FRAC_PI_2 + 0.01,
|
|
std::f32::consts::FRAC_PI_2 - 0.01,
|
|
);
|
|
transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, 0.0);
|
|
}
|
|
}
|
|
|
|
let forward = transform.forward().as_vec3();
|
|
let right = transform.right().as_vec3();
|
|
let up = Vec3::Y;
|
|
let mut wish = Vec3::ZERO;
|
|
|
|
if looking {
|
|
if keys.pressed(KeyCode::KeyW) {
|
|
wish += forward;
|
|
}
|
|
if keys.pressed(KeyCode::KeyS) {
|
|
wish -= forward;
|
|
}
|
|
if keys.pressed(KeyCode::KeyD) {
|
|
wish += right;
|
|
}
|
|
if keys.pressed(KeyCode::KeyA) {
|
|
wish -= right;
|
|
}
|
|
if keys.pressed(KeyCode::KeyE) {
|
|
wish += up;
|
|
}
|
|
if keys.pressed(KeyCode::KeyQ) {
|
|
wish -= up;
|
|
}
|
|
}
|
|
|
|
let sprint = if keys.pressed(KeyCode::ShiftLeft) {
|
|
3.0
|
|
} else {
|
|
1.0
|
|
};
|
|
transform.translation += wish.normalize_or_zero() * camera.fly_speed * sprint * dt;
|
|
transform.translation += forward * scroll_delta * camera.fly_speed * 0.08;
|
|
|
|
if panning {
|
|
let delta = mouse_motion.delta;
|
|
let distance_scale = transform.translation.length().max(1.0).sqrt();
|
|
transform.translation +=
|
|
(-right * delta.x + up * delta.y) * camera.pan_speed * distance_scale;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn restore_editor_camera_cursor(
|
|
input_state: &mut EditorCameraInputState,
|
|
cursor: &mut CursorOptions,
|
|
) {
|
|
if input_state.cursor_hidden_for_look {
|
|
cursor.visible = input_state.cursor_visible_before_look;
|
|
input_state.cursor_hidden_for_look = false;
|
|
}
|
|
}
|