use bevy::{
camera::RenderTarget,
image::ImageSampler,
prelude::*,
render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages},
ui::{UiGlobalTransform, widget::ViewportNode},
};
use bevy_infinite_grid::{InfiniteGridBundle, InfiniteGridPlugin};
use jackdaw_camera::{JackdawCameraPlugin, JackdawCameraSettings};
use bevy::ecs::system::SystemParam;
use crate::selection::{Selected, Selection};
use jackdaw_widgets::file_browser::FileBrowserItem;
#[derive(Component)]
pub struct MainViewportCamera;
const DEFAULT_VIEWPORT_WIDTH: u32 = 1280;
const DEFAULT_VIEWPORT_HEIGHT: u32 = 720;
#[derive(Component)]
pub struct SceneViewport;
#[derive(SystemParam)]
pub(crate) struct ViewportCursor<'w, 's> {
pub camera:
Query<'w, 's, (&'static Camera, &'static GlobalTransform), With<MainViewportCamera>>,
pub windows: Query<'w, 's, &'static Window>,
pub viewport:
Query<'w, 's, (&'static ComputedNode, &'static UiGlobalTransform), With<SceneViewport>>,
}
#[derive(SystemParam)]
pub(crate) struct InteractionGuards<'w> {
pub gizmo_drag: Res<'w, crate::gizmos::GizmoDragState>,
pub gizmo_hover: Res<'w, crate::gizmos::GizmoHoverState>,
pub modal: Res<'w, crate::modal_transform::ModalTransformState>,
pub viewport_drag: Res<'w, crate::modal_transform::ViewportDragState>,
pub draw_state: Res<'w, crate::draw_brush::DrawBrushState>,
pub edit_mode: Res<'w, crate::brush::EditMode>,
pub terrain_edit_mode: Res<'w, crate::terrain::TerrainEditMode>,
}
#[derive(Resource, Default)]
pub struct CameraFlyActive(pub bool);
pub struct ViewportPlugin;
impl Plugin for ViewportPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((JackdawCameraPlugin, InfiniteGridPlugin))
.init_resource::<CameraBookmarks>()
.init_resource::<CameraFlyActive>()
.add_systems(
OnEnter(crate::AppState::Editor),
setup_viewport.after(crate::spawn_layout),
)
.add_systems(
Update,
(update_camera_enabled, handle_camera_keys).in_set(crate::EditorInteraction),
)
.add_systems(
Update,
disable_camera_on_dialog
.run_if(in_state(crate::AppState::Editor))
.run_if(not(crate::no_dialog_open)),
);
}
}
pub(crate) fn setup_viewport(
mut commands: Commands,
mut images: ResMut<Assets<Image>>,
viewport_query: Single<Entity, With<SceneViewport>>,
) {
let size = Extent3d {
width: DEFAULT_VIEWPORT_WIDTH,
height: DEFAULT_VIEWPORT_HEIGHT,
depth_or_array_layers: 1,
};
let mut image = Image::new_fill(
size,
TextureDimension::D2,
&[0, 0, 0, 255],
TextureFormat::Bgra8UnormSrgb,
default(),
);
image.texture_descriptor.usage =
TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT;
image.sampler = ImageSampler::linear();
let image_handle = images.add(image);
let camera = commands
.spawn((
MainViewportCamera,
crate::EditorEntity,
Camera3d::default(),
Camera {
order: -1,
..default()
},
RenderTarget::Image(image_handle.into()),
Transform::from_xyz(0.0, 4.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y),
JackdawCameraSettings::default(),
))
.id();
commands.spawn((crate::EditorEntity, InfiniteGridBundle::default()));
commands
.entity(*viewport_query)
.insert(ViewportNode::new(camera))
.observe(handle_viewport_drop);
}
fn handle_viewport_drop(
event: On<Pointer<DragDrop>>,
file_items: Query<&FileBrowserItem>,
parents: Query<&ChildOf>,
windows: Query<&Window>,
camera_query: Query<(&Camera, &GlobalTransform), With<MainViewportCamera>>,
viewport_query: Query<(&ComputedNode, &UiGlobalTransform), With<SceneViewport>>,
snap_settings: Res<crate::snapping::SnapSettings>,
mut commands: Commands,
) {
let item = find_ancestor_component(event.dropped, &file_items, &parents);
let Some(item) = item else {
return;
};
let path_lower = item.path.to_lowercase();
let is_gltf = path_lower.ends_with(".gltf") || path_lower.ends_with(".glb");
let is_template = path_lower.ends_with(".template.json");
let is_jsn = path_lower.ends_with(".jsn");
if !is_gltf && !is_template && !is_jsn {
return;
}
let Ok(window) = windows.single() else {
return;
};
let Some(cursor_pos) = window.cursor_position() else {
return;
};
let Ok((camera, cam_tf)) = camera_query.single() else {
return;
};
let position =
cursor_to_ground_plane(cursor_pos, camera, cam_tf, &viewport_query).unwrap_or(Vec3::ZERO);
let ctrl = false; let snapped_pos = snap_settings.snap_translate_vec3_if(position, ctrl);
let path = item.path.clone();
if is_jsn {
commands.queue(move |world: &mut World| {
crate::entity_templates::instantiate_jsn_prefab(world, &path, snapped_pos);
});
} else if is_template {
commands.queue(move |world: &mut World| {
crate::entity_templates::instantiate_template(world, &path, snapped_pos);
});
} else {
commands.queue(move |world: &mut World| {
crate::entity_ops::spawn_gltf_in_world(world, &path, snapped_pos);
});
}
}
pub(crate) fn cursor_to_ground_plane(
cursor_pos: Vec2,
camera: &Camera,
cam_tf: &GlobalTransform,
viewport_query: &Query<(&ComputedNode, &UiGlobalTransform), With<SceneViewport>>,
) -> Option<Vec3> {
let viewport_cursor = if let Ok((computed, vp_transform)) = viewport_query.single() {
let scale = computed.inverse_scale_factor();
let vp_pos = vp_transform.translation * scale;
let vp_size = computed.size() * scale;
let vp_top_left = vp_pos - vp_size / 2.0;
let local = cursor_pos - vp_top_left;
let target_size = camera.logical_viewport_size().unwrap_or(vp_size);
local * target_size / vp_size
} else {
cursor_pos
};
let ray = camera.viewport_to_world(cam_tf, viewport_cursor).ok()?;
if ray.direction.y.abs() < 1e-6 {
return None; }
let t = -ray.origin.y / ray.direction.y;
if t < 0.0 {
return None; }
Some(ray.origin + *ray.direction * t)
}
fn find_ancestor_component<'a, C: Component>(
mut entity: Entity,
query: &'a Query<&C>,
parents: &Query<&ChildOf>,
) -> Option<&'a C> {
loop {
if let Ok(component) = query.get(entity) {
return Some(component);
}
if let Ok(child_of) = parents.get(entity) {
entity = child_of.0;
} else {
return None;
}
}
}
fn disable_camera_on_dialog(mut camera_query: Query<&mut JackdawCameraSettings>) {
for mut settings in &mut camera_query {
settings.enabled = false;
}
}
fn update_camera_enabled(
windows: Query<&Window>,
viewport_node: Single<(&ComputedNode, &UiGlobalTransform), With<SceneViewport>>,
mut camera_query: Query<&mut JackdawCameraSettings>,
modal: Res<crate::modal_transform::ModalTransformState>,
input_focus: Res<bevy::input_focus::InputFocus>,
blockers: Query<(), With<crate::BlocksCameraInput>>,
mouse: Res<ButtonInput<MouseButton>>,
mut fly_state: ResMut<CameraFlyActive>,
) {
if mouse.just_released(MouseButton::Right) {
fly_state.0 = false;
}
let Ok(window) = windows.single() else {
return;
};
let (computed, vp_transform) = *viewport_node;
let scale = computed.inverse_scale_factor();
let vp_pos = vp_transform.translation * scale;
let vp_size = computed.size() * scale;
let vp_top_left = vp_pos - vp_size / 2.0;
let vp_bottom_right = vp_pos + vp_size / 2.0;
let hovered = window.cursor_position().is_some_and(|cursor_pos| {
cursor_pos.x >= vp_top_left.x
&& cursor_pos.x <= vp_bottom_right.x
&& cursor_pos.y >= vp_top_left.y
&& cursor_pos.y <= vp_bottom_right.y
});
if mouse.just_pressed(MouseButton::Right) && hovered {
fly_state.0 = true;
}
let modal_active = modal.active.is_some();
let text_focused = input_focus.0.is_some();
let overlay_blocking = !blockers.is_empty();
let should_enable =
(hovered || fly_state.0) && !modal_active && !text_focused && !overlay_blocking;
for mut settings in &mut camera_query {
settings.enabled = should_enable;
}
}
#[derive(Resource, Default)]
pub struct CameraBookmarks {
pub slots: [Option<CameraBookmark>; 9],
}
#[derive(Clone, Copy)]
pub struct CameraBookmark {
pub transform: Transform,
}
fn handle_camera_keys(
keyboard: Res<ButtonInput<KeyCode>>,
keybinds: Res<crate::keybinds::KeybindRegistry>,
selection: Res<Selection>,
selected_transforms: Query<&GlobalTransform, With<Selected>>,
mut camera_query: Query<&mut Transform, With<JackdawCameraSettings>>,
mut bookmarks: ResMut<CameraBookmarks>,
modal: Res<crate::modal_transform::ModalTransformState>,
edit_mode: Res<crate::brush::EditMode>,
) {
use crate::keybinds::EditorAction;
if modal.active.is_some() {
return;
}
if keybinds.just_pressed(EditorAction::FocusSelected, &keyboard) {
if let Some(primary) = selection.primary() {
if let Ok(global_tf) = selected_transforms.get(primary) {
let target = global_tf.translation();
let scale = global_tf.compute_transform().scale;
let dist = (scale.length() * 3.0).max(5.0);
for mut transform in &mut camera_query {
let forward = transform.forward().as_vec3();
transform.translation = target - forward * dist;
*transform = transform.looking_at(target, Vec3::Y);
}
}
}
}
let save_actions = [
(EditorAction::SaveBookmark1, 0),
(EditorAction::SaveBookmark2, 1),
(EditorAction::SaveBookmark3, 2),
(EditorAction::SaveBookmark4, 3),
(EditorAction::SaveBookmark5, 4),
(EditorAction::SaveBookmark6, 5),
(EditorAction::SaveBookmark7, 6),
(EditorAction::SaveBookmark8, 7),
(EditorAction::SaveBookmark9, 8),
];
let load_actions = [
(EditorAction::LoadBookmark1, 0),
(EditorAction::LoadBookmark2, 1),
(EditorAction::LoadBookmark3, 2),
(EditorAction::LoadBookmark4, 3),
(EditorAction::LoadBookmark5, 4),
(EditorAction::LoadBookmark6, 5),
(EditorAction::LoadBookmark7, 6),
(EditorAction::LoadBookmark8, 7),
(EditorAction::LoadBookmark9, 8),
];
for (action, index) in save_actions {
if keybinds.just_pressed(action, &keyboard) {
for transform in &camera_query {
bookmarks.slots[index] = Some(CameraBookmark {
transform: *transform,
});
}
}
}
for (action, index) in load_actions {
if keybinds.just_pressed(action, &keyboard) && *edit_mode == crate::brush::EditMode::Object
{
if let Some(bookmark) = bookmarks.slots[index] {
for mut transform in &mut camera_query {
*transform = bookmark.transform;
}
}
}
}
}