From 0d374dc45e38c35a51b5bbe93702cd36ab5fbb44 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Wed, 23 Aug 2023 08:55:40 -0700 Subject: [PATCH] kinda fixed scene/animation buttons -- also kinda reworked asset/button layout a lot... --- Cargo.lock | 1 + Cargo.toml | 1 + bin/editor.rs | 1019 +++++++++++++++++++++++++++++++++++++++---------- src/ui.rs | 212 +++++++--- 4 files changed, 967 insertions(+), 266 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05a6a15..929c24f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2226,6 +2226,7 @@ version = "0.1.0" dependencies = [ "bevy", "bevy_rapier3d", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6d6e957..e8fdaf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ path = "bin/chars.rs" [dependencies] bevy = "0.11" bevy_rapier3d = "*" +serde = "1" # From rapier docs [profile.dev.package.bevy_rapier3d] diff --git a/bin/editor.rs b/bin/editor.rs index b1ea166..ba9be30 100644 --- a/bin/editor.rs +++ b/bin/editor.rs @@ -2,25 +2,43 @@ // // Editor for creating Monologue Trees levels // +// BUGS: +// * Cannot view scene when selected, WTF +// * Scene and Animation tabs are whacked +// // TODO: -// * Tree Organization: GLTF contains Animations and Scenes -// * Camera can only select one at a time. +// * (medium) Spawn gltf scene +// * (medium) Load default scene when gltf selected +// * (medium) Set gltf to active/inactive +// * (medium) Play individual animation(s) +// * Only select one at a time. // * (hard) Better Colorscheme // * (medium) Visual errors for bad GLTFs -// * (medium) Collapsable containers (Gltfs, Animations, Scenes, Audio Clips, Cameras) -// * (medium) Show/hide entire UI // * (medium) Spawn clicked scene // * (medium) Play clicked animation -// * (idea) Use enum instead of markers for exclusive UI +// * (easy) Play all animations // * (medium) Add fonts similar to Audios based on inspect-fonts -// * (hard) Add Dialogs (requires text box UI, saving, loading) +// * (hard) Add Dialogs (requires text box UI, saving, loading). +// +// Asset types: +// * Audios (done) +// * Loop individual +// * Stop all +// * Gltfs (doing) +// * Scenes +// * Animations +// * Play/Pause all +// * Fonts +// * Monologues use bevy::{ - asset::{AssetPath, Assets}, + asset::{Asset, Assets}, + asset::{AssetLoader, LoadContext, LoadedAsset}, + audio::PlaybackMode, + ecs::system::EntityCommands, gltf::Gltf, - input::{keyboard::KeyboardInput, ButtonState}, prelude::*, - utils::HashSet, + utils::BoxedFuture, }; use monologue_trees::{debug::*, ui}; @@ -39,28 +57,68 @@ fn main() { ui::GameUiPlugin, )) .init_resource::() + .add_asset::() + .init_asset_loader::() + .add_event::>() + .add_event::>() .add_systems(Startup, initialize_ui) .add_systems( Update, - (import_files, import_audio, import_gltf, play_audio), + ( + import_files, + audio_ui, + gltf_ui, + fonts_ui, + texts_ui, + cameras_ui, + manage_active_gltf, + manage_gltf_scene_ui, + manage_gltf_animation_ui, + scenes_ui, + animations_ui, + spawn_scenes, + manage_camera, + play_animation, + play_audio, + ), ) .run(); } #[derive(Resource, Default)] -struct AssetRegistry(Vec); +pub struct AssetRegistry(Vec); -#[derive(Resource)] -struct Styles { - button: Style, - button_hovered: Style, - container: Style, +#[derive(Event)] +pub enum CustomAssetEvent { + Add { handle: Handle, name: String }, + Remove { handle: Handle }, + Clear, } +#[derive(Debug, Component)] +pub struct TabRoot; + +#[derive(Debug, Component)] +pub struct LevelRoot; + +#[derive(Debug, Component)] +pub enum Minimize { + Open, + Closed, +} + +#[derive(Debug, Component)] +pub struct EditorCamera; + fn initialize_ui(mut commands: Commands) { + // Empty entity for populating the level being edited + commands.spawn(LevelRoot); + commands.spawn(( - Camera2dBundle { ..default() }, + Camera3dBundle { ..default() }, UiCameraConfig { show_ui: true }, + Name::new("Editor Camera"), + EditorCamera, )); let base_style = Style { @@ -74,82 +132,144 @@ fn initialize_ui(mut commands: Commands) { commands .spawn(NodeBundle { style: Style { - width: Val::Percent(50.0), - height: Val::Percent(50.0), + border: UiRect::all(Val::Px(1.0)), + margin: UiRect::all(Val::Px(5.0)), + padding: UiRect::all(Val::Px(5.0)), flex_direction: FlexDirection::Row, - ..base_style.clone() + overflow: Overflow::clip(), + ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), - z_index: ZIndex::Local(100), ..default() }) .with_children(|parent| { - spawn_tabtree::(parent, "Audio".into(), &base_style); - spawn_tabtree::(parent, "Gltf".into(), &base_style); - }); - - commands.insert_resource(Styles { - button: Style { ..default() }, - button_hovered: Style { ..default() }, - container: Style { ..default() }, - }) -} + // HACK: This is super janky but I think we need it like this for UI layout rules + let mut content_containers: Vec<(String, Entity)> = Vec::new(); -fn spawn_tabtree( - parent: &mut ChildBuilder, - title: String, - base_style: &Style, -) { - parent - .spawn(( - NodeBundle { - style: Style { - ..base_style.clone() - }, - background_color: Color::WHITE.into(), - border_color: Color::BLACK.into(), - z_index: ZIndex::Local(20), - ..default() - }, - Interaction::default(), - )) - .with_children(|parent| { - // Spawn button container - let container = parent + // Containers with asset content + parent .spawn(( NodeBundle { style: Style { - ..base_style.clone() + border: UiRect::all(Val::Px(1.0)), + margin: UiRect::all(Val::Px(5.0)), + padding: UiRect::all(Val::Px(5.0)), + flex_direction: FlexDirection::Column, + overflow: Overflow::clip(), + justify_content: JustifyContent::FlexStart, + ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), - z_index: ZIndex::Local(10), ..default() }, - T::default(), - ui::Scroll, - Interaction::default(), ui::Sorting(2), )) - .id(); + .with_children(|parent| { + content_containers.push(spawn_tab_container::( + "Font", + parent, + &base_style, + )); + content_containers.push(spawn_tab_container::( + "Audio", + parent, + &base_style, + )); + content_containers.push(spawn_tab_container::( + "Gltf", + parent, + &base_style, + )); + content_containers.push(spawn_tab_container::( + "Scene", + parent, + &base_style, + )); + content_containers.push(spawn_tab_container::( + "Animation", + parent, + &base_style, + )); + content_containers.push(spawn_tab_container::( + "Camera", + parent, + &base_style, + )); + }); - // Spawn widget - parent.spawn(( - ButtonBundle { + // Container for tabs that open/close containers + parent + .spawn(( + NodeBundle { + style: Style { + border: UiRect::all(Val::Px(1.0)), + margin: UiRect::all(Val::Px(5.0)), + padding: UiRect::all(Val::Px(5.0)), + flex_direction: FlexDirection::Column, + overflow: Overflow::clip(), + ..default() + }, + background_color: Color::WHITE.into(), + border_color: Color::BLACK.into(), + ..default() + }, + ui::Sorting(1), + )) + .with_children(|parent| { + let b = ButtonBundle { + style: Style { + ..base_style.clone() + }, + background_color: Color::WHITE.into(), + border_color: Color::BLACK.into(), + ..default() + }; + content_containers.iter().for_each(|(name, target)| { + parent.spawn(( + b.clone(), + ui::Title { + name: name.clone(), + ..default() + }, + ui::Collapse { target: *target }, + )); + }); + }); + }); +} + +fn spawn_tab_container( + title: &'static str, + parent: &mut ChildBuilder, + base_style: &Style, +) -> (String, Entity) { + ( + title.into(), + parent + .spawn(( + NodeBundle { style: Style { + display: Display::None, + top: Val::Px(0.0), ..base_style.clone() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), - z_index: ZIndex::Local(15), + z_index: ZIndex::Local(100), ..default() }, - ui::Title { name: title }, - ui::Collapse { target: container }, - ui::Sorting(1), - )); - }); + ui::Title { + name: title.into(), + ..default() + }, + T::default(), + ui::Scroll, + Interaction::default(), + )) + .id(), + ) } fn import_files( @@ -175,18 +295,17 @@ fn import_files( use audio::*; mod audio { - use bevy::audio::PlaybackMode; use super::*; #[derive(Debug, Component, Default)] pub struct AudioWidget; - pub fn import_audio( + pub fn audio_ui( mut events: EventReader>, mut commands: Commands, - root: Query>, - current: Query<(Entity, &Handle)>, + widget: Query>, + current: Query<(Entity, &ui::TargetAsset)>, server: Res, ) { events @@ -195,84 +314,64 @@ mod audio { AssetEvent::Created { handle } | AssetEvent::Removed { handle } | AssetEvent::Modified { handle } => { - if let Some(asset_path) = server.get_handle_path(handle.clone()) { - if let Some(extension) = asset_path.path().extension() { - extension == "ogg" - } else { - false - } - } else { - false - } + has_extensions(&server, handle.clone(), &["ogg"]) } }) - .for_each(|event| { - let create = |commands: &mut Commands, handle: Handle| { - commands.entity(root.single()).with_children(|parent| { - let name = { - if let Some(asset_path) = server.get_handle_path(handle.clone()) { - if let Some(stem) = asset_path.path().file_stem() { - if let Some(val) = stem.to_str() { - String::from(val) - } else { - String::from("???") - } - } else { - String::from("???") - } - } else { - String::from("???") - } - }; - let settings = PlaybackSettings { + .for_each(|event| match event { + AssetEvent::Created { handle } => { + info!("Asset created! {:?}", event); + let id = create_asset_button( + &widget, + &mut commands, + ui::TargetAsset { + handle: handle.clone(), + }, + get_asset_name(&server, handle.clone()), + ); + commands.entity(id).insert(AudioSourceBundle { + source: handle.clone(), + settings: PlaybackSettings { mode: PlaybackMode::Loop, paused: true, ..default() - }; - parent.spawn(( - AudioSourceBundle { - source: handle, - settings, - }, - ButtonBundle { - style: Style { - border: UiRect::all(Val::Px(1.0)), - margin: UiRect::all(Val::Px(5.0)), - padding: UiRect::all(Val::Px(5.0)), - ..default() - }, - border_color: Color::BLACK.into(), - ..default() - }, - ui::Title { name }, - )); + }, + }); + } + AssetEvent::Removed { handle } => { + info!("Asset removed! {:?}", event); + destroy_asset_button( + ¤t, + &mut commands, + &ui::TargetAsset { + handle: handle.clone(), + }, + ); + } + AssetEvent::Modified { handle } => { + info!("Asset modified! {:?}", event); + destroy_asset_button( + ¤t, + &mut commands, + &ui::TargetAsset { + handle: handle.clone(), + }, + ); + let id = create_asset_button( + &widget, + &mut commands, + ui::TargetAsset { + handle: handle.clone(), + }, + get_asset_name(&server, handle.clone()), + ); + commands.entity(id).insert(AudioSourceBundle { + source: handle.clone(), + settings: PlaybackSettings { + mode: PlaybackMode::Loop, + paused: true, + ..default() + }, }); - }; - let destroy = |commands: &mut Commands, handle: Handle| { - if let Some(entity) = current.iter().find_map(|(entity, current)| { - if *current == handle { - Some(entity) - } else { - None - } - }) { - commands.entity(entity).despawn_recursive(); - } - }; - match event { - AssetEvent::Created { handle } => { - info!("Asset created! {:?}", event); - create(&mut commands, handle.clone()); - } - AssetEvent::Removed { handle } => { - info!("Asset removed! {:?}", event); - destroy(&mut commands, handle.clone()); - } - AssetEvent::Modified { handle } => { - info!("Asset modified! {:?}", event); - destroy(&mut commands, handle.clone()); - create(&mut commands, handle.clone()); - } } }); } @@ -295,6 +394,110 @@ mod audio { } } +use assets::*; +mod assets { + + use super::*; + + pub fn get_asset_name(server: &AssetServer, handle: Handle) -> String { + if let Some(asset_path) = server.get_handle_path(handle.clone()) { + if let Some(stem) = asset_path.path().file_stem() { + if let Some(val) = stem.to_str() { + String::from(val) + } else { + String::from("???") + } + } else { + String::from("???") + } + } else { + String::from("???") + } + } + + pub fn create_asset_button( + root: &Query>, + commands: &mut Commands, + target: ui::TargetAsset, + name: String, + ) -> Entity { + commands + .spawn(( + target, + ButtonBundle { + style: Style { + border: UiRect::all(Val::Px(1.0)), + margin: UiRect::all(Val::Px(5.0)), + padding: UiRect::all(Val::Px(5.0)), + ..default() + }, + border_color: Color::BLACK.into(), + ..default() + }, + ui::Title { name, ..default() }, + )) + .set_parent(root.single()) + .id() + } + + pub fn create_entity_button( + root: &Query>, + commands: &mut Commands, + target: ui::TargetEntity, + name: String, + ) -> Entity { + commands + .spawn(( + target, + ButtonBundle { + style: Style { + border: UiRect::all(Val::Px(1.0)), + margin: UiRect::all(Val::Px(5.0)), + padding: UiRect::all(Val::Px(5.0)), + ..default() + }, + border_color: Color::BLACK.into(), + ..default() + }, + ui::Title { name, ..default() }, + )) + .set_parent(root.single()) + .id() + } + + pub fn destroy_asset_button( + current: &Query<(Entity, &ui::TargetAsset)>, + commands: &mut Commands, + target: &ui::TargetAsset, + ) { + if let Some(entity) = current.iter().find_map(|(entity, this)| { + if this.handle == target.handle { + Some(entity) + } else { + None + } + }) { + commands.entity(entity).despawn_recursive(); + } + } + + pub fn has_extensions( + server: &AssetServer, + handle: Handle, + extensions: &[&'static str], + ) -> bool { + if let Some(asset_path) = server.get_handle_path(handle.clone()) { + if let Some(extension) = asset_path.path().extension() { + extensions.iter().any(|&check| check == extension) + } else { + false + } + } else { + false + } + } +} + use gltf::*; mod gltf { use super::*; @@ -302,11 +505,12 @@ mod gltf { #[derive(Debug, Component, Default)] pub struct GltfWidget; - pub fn import_gltf( + // TODO: Mark selected gltf as active ~single exclusive~ + pub fn gltf_ui( mut events: EventReader>, mut commands: Commands, - root: Query>, - current: Query<(Entity, &Handle)>, + widget: Query>, + current: Query<(Entity, &ui::TargetAsset)>, server: Res, ) { events @@ -315,77 +519,468 @@ mod gltf { AssetEvent::Created { handle } | AssetEvent::Removed { handle } | AssetEvent::Modified { handle } => { - if let Some(asset_path) = server.get_handle_path(handle.clone()) { - if let Some(extension) = asset_path.path().extension() { - extension == "gltf" || extension == "glb" - } else { - false - } - } else { - false + has_extensions(&server, handle.clone(), &["gltf", "glb"]) + } + }) + .for_each(|event| match event { + AssetEvent::Created { handle } => { + info!("Asset created! {:?}", event); + create_asset_button( + &widget, + &mut commands, + ui::TargetAsset { + handle: handle.clone(), + }, + get_asset_name(&server, handle.clone()), + ); + } + AssetEvent::Removed { handle } => { + info!("Asset removed! {:?}", event); + destroy_asset_button( + ¤t, + &mut commands, + &ui::TargetAsset { + handle: handle.clone(), + }, + ); + } + AssetEvent::Modified { handle } => { + info!("Asset modified! {:?}", event); + destroy_asset_button( + ¤t, + &mut commands, + &ui::TargetAsset { + handle: handle.clone(), + }, + ); + create_asset_button( + &widget, + &mut commands, + ui::TargetAsset { + handle: handle.clone(), + }, + get_asset_name(&server, handle.clone()), + ); + } + }); + } + + pub fn manage_active_gltf( + events: Query< + (Entity, &Parent, &Interaction, Option<&ui::Active>), + (With