317 lines
10 KiB
Rust
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,
|
|
}
|