use bevy::{
asset::{embedded_asset, load_embedded_asset},
camera::{RenderTarget, visibility::RenderLayers},
core_pipeline::oit::OrderIndependentTransparencySettings,
gizmos::{GizmoAsset, retained::Gizmo},
image::ImageSampler,
prelude::*,
render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages},
ui::{UiGlobalTransform, widget::ViewportNode},
};
use bevy_enhanced_input::prelude::{Press, *};
use bevy_infinite_grid::{InfiniteGridBundle, InfiniteGridPlugin};
use jackdaw_api::prelude::*;
use jackdaw_camera::{JackdawCameraPlugin, JackdawCameraSettings};
use bevy::ecs::system::SystemParam;
use crate::core_extension::CoreExtensionInputContext;
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(Component)]
pub(crate) struct ViewportPanelHost {
pub camera: Entity,
pub grid: Entity,
pub axis_indicator: Entity,
}
#[derive(Component)]
pub struct AxisIndicator {
pub camera: Entity,
}
#[derive(Resource)]
struct AxisIndicatorAsset(Handle<GizmoAsset>);
#[derive(Component)]
pub struct ViewportGrid(pub Entity);
#[derive(Resource)]
pub(crate) struct ViewportLayerCounter(usize);
impl Default for ViewportLayerCounter {
fn default() -> Self {
Self(1)
}
}
impl ViewportLayerCounter {
fn next(&mut self) -> usize {
self.0 += 1;
self.0
}
}
#[derive(Resource, Default, Debug, Clone, Copy)]
pub struct ActiveViewport {
pub camera: Option<Entity>,
pub ui_node: Option<Entity>,
}
#[derive(SystemParam)]
pub(crate) struct ViewportCursor<'w, 's> {
pub windows: Query<'w, 's, &'static Window>,
cameras: Query<'w, 's, (&'static Camera, &'static GlobalTransform), With<MainViewportCamera>>,
viewports: Query<
'w,
's,
(
&'static ComputedNode,
&'static UiGlobalTransform,
&'static ViewportNode,
),
With<SceneViewport>,
>,
active: Res<'w, ActiveViewport>,
}
impl ViewportCursor<'_, '_> {
pub fn camera(&self) -> Option<(&Camera, &GlobalTransform)> {
let camera_entity = self.active.camera?;
self.cameras.get(camera_entity).ok()
}
pub fn viewport(&self) -> Option<(&ComputedNode, &UiGlobalTransform)> {
let ui_entity = self.active.ui_node?;
self.viewports.get(ui_entity).ok().map(|(c, t, _)| (c, t))
}
pub fn camera_entity(&self) -> Option<Entity> {
self.active.camera
}
pub fn viewport_entity(&self) -> Option<Entity> {
self.active.ui_node
}
pub fn camera_for(&self, entity: Entity) -> Option<(&Camera, &GlobalTransform)> {
self.cameras.get(entity).ok()
}
pub fn viewport_for(&self, entity: Entity) -> Option<(&ComputedNode, &UiGlobalTransform)> {
self.viewports.get(entity).ok().map(|(c, t, _)| (c, t))
}
pub fn viewport_cursor_for(
&self,
camera: &Camera,
viewport_entity: Entity,
cursor: Vec2,
) -> Option<Vec2> {
let (computed, vp_tf, _) = self.viewports.get(viewport_entity).ok()?;
let map = crate::viewport_util::ViewportRemap::new(camera, computed, vp_tf);
let local = cursor - map.top_left;
if local.x >= 0.0 && local.y >= 0.0 && local.x <= map.vp_size.x && local.y <= map.vp_size.y
{
Some(local * map.remap)
} else {
None
}
}
}
#[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::<CameraFlyActive>()
.init_resource::<ActiveViewport>()
.init_resource::<ViewportLayerCounter>()
.insert_resource(GlobalAmbientLight::NONE)
.add_systems(Startup, init_axis_indicator_asset)
.add_systems(
OnEnter(crate::AppState::Editor),
setup_viewport.after(crate::init_layout),
)
.add_observer(on_viewport_panel_despawn)
.add_systems(
Update,
(
update_active_viewport,
camera_bookmark_keys,
crate::view_ops::axis_view_keys,
)
.in_set(crate::EditorInteractionSystems),
)
.add_systems(
Update,
disable_camera_on_dialog
.run_if(in_state(crate::AppState::Editor))
.run_if(not(crate::no_dialog_open)),
);
embedded_asset!(
app,
"../assets/environment_maps/voortrekker_interior_1k_diffuse.ktx2"
);
embedded_asset!(
app,
"../assets/environment_maps/voortrekker_interior_1k_specular.ktx2"
);
}
}
pub(crate) fn setup_viewport() {}
fn init_axis_indicator_asset(mut commands: Commands, mut assets: ResMut<Assets<GizmoAsset>>) {
let mut asset = GizmoAsset::default();
asset.line(Vec3::ZERO, Vec3::X, crate::default_style::AXIS_X);
asset.line(Vec3::ZERO, Vec3::Y, crate::default_style::AXIS_Y);
asset.line(Vec3::ZERO, Vec3::Z, crate::default_style::AXIS_Z);
commands.insert_resource(AxisIndicatorAsset(assets.add(asset)));
}
pub(crate) fn build_viewport_panel(world: &mut World, parent: Entity) {
let image_handle = {
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();
world.resource_mut::<Assets<Image>>().add(image)
};
let assets = world.resource::<AssetServer>().clone();
let env_diffuse = load_embedded_asset!(
&assets,
"../assets/environment_maps/voortrekker_interior_1k_diffuse.ktx2"
);
let env_specular = load_embedded_asset!(
&assets,
"../assets/environment_maps/voortrekker_interior_1k_specular.ktx2"
);
let viewport_layer = world.resource_mut::<ViewportLayerCounter>().next();
let camera_layers = RenderLayers::from_layers(&[0, viewport_layer]);
let grid_layers = RenderLayers::layer(viewport_layer);
let grid = world
.spawn((
crate::EditorEntity,
InfiniteGridBundle::default(),
grid_layers.clone(),
))
.id();
let camera = world
.spawn((
MainViewportCamera,
crate::EditorEntity,
Camera3d::default(),
EnvironmentMapLight {
diffuse_map: env_diffuse,
specular_map: env_specular,
intensity: 500.0,
..default()
},
OrderIndependentTransparencySettings::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),
Msaa::Off,
JackdawCameraSettings::default(),
ViewportConfig::default(),
camera_layers,
ViewportGrid(grid),
))
.id();
let asset_handle = world.resource::<AxisIndicatorAsset>().0.clone();
let axis_indicator = world
.spawn((
crate::EditorEntity,
AxisIndicator { camera },
Gizmo {
handle: asset_handle,
depth_bias: -0.5,
..default()
},
Transform::default(),
Visibility::Inherited,
grid_layers.clone(),
))
.id();
world.spawn((ChildOf(parent), crate::layout::viewport_with_toolbar()));
let scene_vp = find_descendant_with::<SceneViewport>(world, parent);
if let Some(scene_vp) = scene_vp {
world.entity_mut(scene_vp).insert(ViewportNode::new(camera));
world.entity_mut(scene_vp).observe(handle_viewport_drop);
} else {
warn!("build_viewport_panel: SceneViewport descendant not found under parent");
}
world.entity_mut(parent).insert(ViewportPanelHost {
camera,
grid,
axis_indicator,
});
}
fn find_descendant_with<T: Component>(world: &mut World, root: Entity) -> Option<Entity> {
let mut stack = vec![root];
let mut q_t = world.query_filtered::<Entity, With<T>>();
let with_t: std::collections::HashSet<Entity> = q_t.iter(world).collect();
while let Some(entity) = stack.pop() {
if with_t.contains(&entity) && entity != root {
return Some(entity);
}
if let Some(children) = world.entity(entity).get::<Children>() {
stack.extend(children.iter());
}
}
None
}
pub(crate) fn on_viewport_panel_despawn(
trigger: On<Despawn, ViewportPanelHost>,
hosts: Query<&ViewportPanelHost>,
mut commands: Commands,
) {
let entity = trigger.event_target();
if let Ok(host) = hosts.get(entity) {
if let Ok(mut ec) = commands.get_entity(host.camera) {
ec.despawn();
}
if let Ok(mut ec) = commands.get_entity(host.grid) {
ec.despawn();
}
if let Ok(mut ec) = commands.get_entity(host.axis_indicator) {
ec.despawn();
}
}
}
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>>,
active: Res<ActiveViewport>,
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 Some(camera_entity) = active.camera else {
return;
};
let Some(viewport_entity) = active.ui_node else {
return;
};
let Ok((camera, cam_tf)) = camera_query.get(camera_entity) else {
return;
};
let position =
cursor_to_ground_plane_for(cursor_pos, camera, cam_tf, viewport_entity, &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_for(
cursor_pos: Vec2,
camera: &Camera,
cam_tf: &GlobalTransform,
viewport_entity: Entity,
viewport_query: &Query<(&ComputedNode, &UiGlobalTransform), With<SceneViewport>>,
) -> Option<Vec3> {
let viewport_cursor = crate::viewport_util::window_to_viewport_cursor_for(
cursor_pos,
camera,
viewport_entity,
viewport_query,
)?;
raycast_to_ground(camera, cam_tf, viewport_cursor)
}
fn raycast_to_ground(
camera: &Camera,
cam_tf: &GlobalTransform,
viewport_cursor: Vec2,
) -> Option<Vec3> {
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_active_viewport(
windows: Query<&Window>,
viewports: Query<
(Entity, &ComputedNode, &UiGlobalTransform, &ViewportNode),
With<SceneViewport>,
>,
mut active: ResMut<ActiveViewport>,
mut camera_query: Query<(Entity, &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 cursor = windows.single().ok().and_then(Window::cursor_position);
let mut hovered: Option<(Entity, Entity)> = None; if let Some(cursor) = cursor {
for (ui_entity, computed, vp_transform, vp_node) in &viewports {
let scale = computed.inverse_scale_factor();
let vp_pos = vp_transform.translation * scale;
let vp_size = computed.size() * scale;
let top_left = vp_pos - vp_size / 2.0;
let bottom_right = vp_pos + vp_size / 2.0;
if cursor.x >= top_left.x
&& cursor.x <= bottom_right.x
&& cursor.y >= top_left.y
&& cursor.y <= bottom_right.y
{
hovered = Some((ui_entity, vp_node.camera));
break;
}
}
}
if !fly_state.0 {
active.ui_node = hovered.map(|(ui, _)| ui);
active.camera = hovered.map(|(_, cam)| cam);
}
if mouse.just_pressed(MouseButton::Right) && hovered.is_some() {
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 inputs_clear = !modal_active && !text_focused && !overlay_blocking;
let target_camera = active.camera;
let fly_engaged = fly_state.0;
for (entity, mut settings) in &mut camera_query {
let is_target = target_camera == Some(entity);
let should_enable = inputs_clear && (is_target && (hovered.is_some() || fly_engaged));
if settings.enabled != should_enable {
settings.enabled = should_enable;
}
}
}
#[derive(Component, Clone, Default, Debug)]
pub struct ViewportConfig {
pub bookmarks: [Option<CameraBookmark>; 9],
}
#[derive(Clone, Copy, Debug)]
pub struct CameraBookmark {
pub transform: Transform,
}
fn camera_bookmark_keys(
keyboard: Res<ButtonInput<KeyCode>>,
edit_mode: Res<crate::brush::EditMode>,
selection: Res<Selection>,
brushes: Query<(), With<jackdaw_jsn::Brush>>,
modal: Res<crate::modal_transform::ModalTransformState>,
mut commands: Commands,
) {
if modal.active.is_some() {
return;
}
let ctrl = keyboard.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
let in_object_mode = *edit_mode == crate::brush::EditMode::Object;
let conflicts_with_edit_mode_digits =
in_object_mode && selection.primary().is_some_and(|e| brushes.contains(e));
let digits = [
KeyCode::Digit1,
KeyCode::Digit2,
KeyCode::Digit3,
KeyCode::Digit4,
KeyCode::Digit5,
KeyCode::Digit6,
KeyCode::Digit7,
KeyCode::Digit8,
KeyCode::Digit9,
];
for (slot, key) in digits.iter().enumerate() {
if !keyboard.just_pressed(*key) {
continue;
}
if ctrl {
commands
.operator(ViewportBookmarkSaveOp::ID)
.param("slot", slot as i64)
.call();
} else if in_object_mode && !conflicts_with_edit_mode_digits {
commands
.operator(ViewportBookmarkLoadOp::ID)
.param("slot", slot as i64)
.call();
}
}
}
pub(crate) fn add_to_extension(ctx: &mut ExtensionContext) {
ctx.register_operator::<ViewportFocusSelectedOp>()
.register_operator::<ViewportBookmarkSaveOp>()
.register_operator::<ViewportBookmarkLoadOp>();
let ext = ctx.id();
ctx.spawn((
Action::<ViewportFocusSelectedOp>::new(),
ActionOf::<CoreExtensionInputContext>::new(ext),
bindings![(KeyCode::KeyF, Press::default())],
));
}
fn has_primary_selection(selection: Res<Selection>) -> bool {
selection.primary().is_some()
}
#[operator(
id = "viewport.focus_selected",
label = "Focus Selected",
description = "Center the camera on the selected entity.",
is_available = has_primary_selection
)]
pub(crate) fn viewport_focus_selected(
_: In<OperatorParameters>,
active: Res<ActiveViewport>,
selection: Res<Selection>,
selected_transforms: Query<&GlobalTransform, With<Selected>>,
mut camera_query: Query<&mut Transform, With<JackdawCameraSettings>>,
) -> OperatorResult {
let Some(primary) = selection.primary() else {
return OperatorResult::Cancelled;
};
let Ok(global_tf) = selected_transforms.get(primary) else {
return OperatorResult::Cancelled;
};
let target = global_tf.translation();
let scale = global_tf.compute_transform().scale;
let dist = f32::max(scale.length() * 3.0, 5.0);
let Some(camera_entity) = active.camera else {
return OperatorResult::Cancelled;
};
let Ok(mut transform) = camera_query.get_mut(camera_entity) else {
return OperatorResult::Cancelled;
};
let forward = transform.forward().as_vec3();
transform.translation = target - forward * dist;
*transform = transform.looking_at(target, Vec3::Y);
OperatorResult::Finished
}
fn slot_param(params: &OperatorParameters) -> Option<usize> {
let v = params.as_int("slot")?;
(0..9).contains(&v).then_some(v as usize)
}
#[operator(
id = "viewport.bookmark.save",
label = "Save Camera Bookmark",
description = "Save the camera position to a numbered slot.",
params(slot(i64, doc = "Bookmark slot 0..=8."))
)]
pub(crate) fn viewport_bookmark_save(
params: In<OperatorParameters>,
active: Res<ActiveViewport>,
mut cameras: Query<(&Transform, &mut ViewportConfig), With<JackdawCameraSettings>>,
) -> OperatorResult {
let Some(slot) = slot_param(¶ms) else {
return OperatorResult::Cancelled;
};
let Some(camera_entity) = active.camera else {
return OperatorResult::Cancelled;
};
let Ok((transform, mut config)) = cameras.get_mut(camera_entity) else {
return OperatorResult::Cancelled;
};
config.bookmarks[slot] = Some(CameraBookmark {
transform: *transform,
});
OperatorResult::Finished
}
#[operator(
id = "viewport.bookmark.load",
label = "Load Camera Bookmark",
description = "Restore the camera to a previously-saved slot.",
params(slot(i64, doc = "Bookmark slot 0..=8."))
)]
pub(crate) fn viewport_bookmark_load(
params: In<OperatorParameters>,
active: Res<ActiveViewport>,
mut cameras: Query<(&mut Transform, &ViewportConfig), With<JackdawCameraSettings>>,
) -> OperatorResult {
let Some(slot) = slot_param(¶ms) else {
return OperatorResult::Cancelled;
};
let Some(camera_entity) = active.camera else {
return OperatorResult::Cancelled;
};
let Ok((mut transform, config)) = cameras.get_mut(camera_entity) else {
return OperatorResult::Cancelled;
};
let Some(bookmark) = config.bookmarks[slot] else {
return OperatorResult::Cancelled;
};
*transform = bookmark.transform;
OperatorResult::Finished
}