Blacksite/crates/editor/src/ui/helpers.rs
Rbanh 0553a85220
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
Build production-ready editor authoring workflows
2026-07-11 12:41:04 -04:00

317 lines
10 KiB
Rust

//! Spawn helpers, bookmarks, hierarchy tree, and asset labeling.
use bevy::prelude::*;
use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;
use shared::{
AuthoringLightKind, LevelObject, LightDesc, MaterialDesc, PlayerSpawn, PrimitiveShape,
ProjectSun,
};
use sim::Player;
use crate::assets::{snapshot_for_asset, EditorAsset, EditorAssetKind};
use crate::camera::EditorCamera;
use crate::gizmos::{EditorGizmoMode, EditorGizmoSpace};
use crate::history::{set_material_group_with_history, spawn_with_history, EditorEntitySnapshot};
use crate::play::default_player_spawn;
use crate::scene_io::SceneIo;
use crate::viewport::{recall_camera_bookmark, save_camera_bookmark, CameraBookmarks};
pub use super::play_controls::{toggle_play_mode, toggle_play_paused, toggle_possession};
pub use super::selection_ops::{
delete_selection, duplicate_selection, entity_name, focus_editor_camera_on_selection,
is_level_object, selected_level_entities,
};
pub fn set_gizmo_mode(world: &mut World, mode: EditorGizmoMode) {
*world.resource_mut::<EditorGizmoMode>() = mode;
}
pub fn set_gizmo_space(world: &mut World, space: EditorGizmoSpace) {
*world.resource_mut::<EditorGizmoSpace>() = space;
}
pub fn viewport_save_bookmark(world: &mut World) {
let scene_key = world
.resource::<SceneIo>()
.active_path
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "__unsaved__".into());
let Some(transform) = world
.query_filtered::<&Transform, With<EditorCamera>>()
.single(world)
.ok()
.copied()
else {
return;
};
let mut bookmarks = world.resource_mut::<CameraBookmarks>();
save_camera_bookmark(&mut bookmarks, &scene_key, transform);
}
pub fn viewport_recall_bookmark(world: &mut World) {
let scene_key = world
.resource::<SceneIo>()
.active_path
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "__unsaved__".into());
let saved = {
let bookmarks = world.resource::<CameraBookmarks>();
match recall_camera_bookmark(bookmarks, &scene_key) {
Some(transform) => transform,
None => return,
}
};
for mut transform in world
.query_filtered::<&mut Transform, With<EditorCamera>>()
.iter_mut(world)
{
*transform = saved;
}
}
pub fn primitive_snapshot(name: &str, shape: PrimitiveShape, origin: Vec3) -> EditorEntitySnapshot {
let asset = EditorAsset {
label: name.to_string(),
path: None,
folder_path: crate::assets::BUILTINS_FOLDER.to_string(),
kind: EditorAssetKind::Primitive(shape),
};
snapshot_for_asset(&asset, origin).expect("primitive assets are spawnable")
}
pub fn light_snapshot(name: &str, translation: Vec3) -> EditorEntitySnapshot {
EditorEntitySnapshot {
actor_id: None,
actor_kind: shared::ActorKind::Light,
actor_name: None,
name: Some(name.to_string()),
transform: Transform::from_translation(translation),
primitive: None,
brush: None,
static_mesh_renderer: None,
material: None,
material_override: None,
rigid_body: None,
collider: None,
physics: None,
light: Some(LightDesc::default()),
animation_controller: None,
audio_source: None,
audio_listener: None,
player_spawn: false,
model: None,
prefab: None,
prefab_instance: None,
weapon_spawn: None,
trigger_volume: None,
post_process_volume: None,
team_spawn: None,
objective: None,
hierarchy_sibling_index: 0,
editor_visibility: shared::EditorVisibility::default(),
children: Vec::new(),
}
}
pub fn ensure_player_spawn_for_edit(world: &mut World) -> Entity {
if let Some(entity) = world
.query_filtered::<Entity, With<PlayerSpawn>>()
.iter(world)
.next()
{
return entity;
}
let transform = world
.query_filtered::<&Transform, With<Player>>()
.iter(world)
.next()
.copied()
.unwrap_or_else(default_player_spawn);
spawn_with_history(
world,
EditorEntitySnapshot {
actor_id: None,
actor_kind: shared::ActorKind::PlayerSpawn,
actor_name: None,
name: Some("Player Start".to_string()),
transform,
primitive: None,
brush: None,
static_mesh_renderer: None,
material: None,
material_override: None,
rigid_body: None,
collider: None,
physics: None,
light: None,
animation_controller: None,
audio_source: None,
audio_listener: None,
player_spawn: true,
model: None,
prefab: None,
prefab_instance: None,
weapon_spawn: None,
trigger_volume: None,
post_process_volume: None,
team_spawn: None,
objective: None,
hierarchy_sibling_index: 0,
editor_visibility: shared::EditorVisibility::default(),
children: Vec::new(),
},
)
}
/// Removes all authored [`LightDesc`] so project sun/ambient drive outdoor lighting.
pub fn reset_scene_lighting_to_project_defaults(world: &mut World) {
let mut query = world.query_filtered::<Entity, With<LevelObject>>();
let with_lights: Vec<Entity> = query
.iter(world)
.filter(|entity| world.get::<LightDesc>(*entity).is_some())
.collect();
for entity in with_lights {
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
entity_mut.remove::<LightDesc>();
}
}
world
.resource_mut::<crate::scene_io::SceneIo>()
.mark_dirty();
}
/// Removes authored directional lights so [`ProjectSun`] drives outdoor lighting.
pub fn use_project_sun(world: &mut World) {
let mut query = world.query_filtered::<(Entity, &LightDesc), With<LevelObject>>();
let directionals: Vec<Entity> = query
.iter(world)
.filter(|(_, light)| matches!(light.kind, AuthoringLightKind::Directional))
.map(|(e, _)| e)
.collect();
for entity in directionals {
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
entity_mut.remove::<LightDesc>();
}
}
world
.resource_mut::<crate::scene_io::SceneIo>()
.mark_dirty();
}
pub fn create_scene_sun_override_from_project_settings(world: &mut World) -> Entity {
let (transform, light) = {
let sun_illuminance = world
.resource::<settings::ProjectSettings>()
.rendering
.sun_illuminance;
let transform = world
.query_filtered::<&Transform, With<ProjectSun>>()
.iter(world)
.next()
.copied()
.unwrap_or_else(|| {
Transform::from_xyz(0.0, 0.0, 0.0)
.looking_to(Vec3::new(-0.35, -0.85, -0.4), Vec3::Y)
});
let mut light = LightDesc::for_kind(AuthoringLightKind::Directional);
light.intensity = sun_illuminance;
(transform, light)
};
spawn_with_history(
world,
EditorEntitySnapshot {
actor_id: None,
actor_kind: shared::ActorKind::Light,
actor_name: None,
name: Some("Scene Sun".to_string()),
transform,
primitive: None,
brush: None,
static_mesh_renderer: None,
material: None,
material_override: None,
rigid_body: None,
collider: None,
physics: None,
light: Some(light),
animation_controller: None,
audio_source: None,
audio_listener: None,
player_spawn: false,
model: None,
prefab: None,
prefab_instance: None,
weapon_spawn: None,
trigger_volume: None,
post_process_volume: None,
team_spawn: None,
objective: None,
hierarchy_sibling_index: 0,
editor_visibility: shared::EditorVisibility::default(),
children: Vec::new(),
},
)
}
pub fn asset_label(asset: &EditorAsset) -> String {
let prefix = match asset.kind {
EditorAssetKind::Primitive(_) => "Primitive",
EditorAssetKind::Light(_) => "Light",
EditorAssetKind::Model => "Model",
EditorAssetKind::Texture => "Texture",
EditorAssetKind::Material => "Material",
EditorAssetKind::AudioClip => "Audio Clip",
EditorAssetKind::Level => "Level",
EditorAssetKind::Prefab => "Prefab",
EditorAssetKind::PostProcessVolume => "Post Process Volume",
EditorAssetKind::PostProcessEffect => "Post FX",
EditorAssetKind::RenderingProfile => "Rendering Profile",
EditorAssetKind::ShaderSchema => "Shader Schema",
};
format!("{prefix}: {}", asset.label)
}
pub fn apply_texture_to_selection(
world: &mut World,
asset: &EditorAsset,
selected: &SelectedEntities,
) -> Result<(), String> {
let path = asset
.path
.clone()
.ok_or_else(|| format!("Texture has no source path: {}", asset.label))?;
let changes = selected_level_entities(world, selected)
.into_iter()
.map(|entity| {
let mut material = world
.get::<MaterialDesc>(entity)
.cloned()
.unwrap_or_default();
material.base_color_texture = Some(path.clone());
(entity, material)
})
.collect::<Vec<_>>();
set_material_group_with_history(world, changes);
Ok(())
}
pub fn apply_material_asset_to_selection(
world: &mut World,
asset: &EditorAsset,
selected: &SelectedEntities,
) -> Result<(), String> {
crate::assets::materials::apply_material_asset_to_selection(world, asset, selected)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HierarchyNodeKind {
Authored,
Runtime,
Generated,
}