From e159346b8946e9bfa095dbe6a897218a7e516b68 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Mon, 7 Aug 2023 07:31:48 -0700 Subject: [PATCH] Loading animations w/ button --- bin/editor.rs | 250 ++++++++++++++++++++++++++++++++++++++++++++++---- src/ui.rs | 96 +++++++++---------- 2 files changed, 284 insertions(+), 62 deletions(-) diff --git a/bin/editor.rs b/bin/editor.rs index fdab4be..abad074 100644 --- a/bin/editor.rs +++ b/bin/editor.rs @@ -1,10 +1,26 @@ // Monologue Trees Editor // // Editor for creating Monologue Trees levels +// +// TODO: +// * Tree Organization: GLTF contains Animations and Scenes +// * Camera can only select one at a time. +// * (easy) Load audios like current GLTFs +// * (easy) Loop audio enable/disable +// * (easy) Better Colorscheme +// * (easy) Interactive buttons (hover/click) +// * (medium) Visual errors for bad GLTFs +// * (medium) Collapsable containers (Gltfs, Animations, Scenes, Audio Clips, Cameras) +// * (medium) Spawn clicked scene +// * (medium) Play clicked animation +// * (idea) Use enum instead of markers for exclusive UI use bevy::{ + asset::{AssetPath, Assets}, + gltf::Gltf, input::{keyboard::KeyboardInput, ButtonState}, prelude::*, + utils::HashSet, }; use monologue_trees::{debug::*, ui::*}; @@ -22,24 +38,43 @@ fn main() { DebugInfoPlugin, GameUiPlugin, )) + .init_resource::() .add_systems(Startup, (initialize_ui,)) .add_systems( Update, ( + // GLTF Systems load_gltf, unload_gltf, + manage_gltf_ui, + // Scene Systems + manage_scene_ui, + spawn_scene, + // Animation systems + manage_animation_ui, + // Camera systems + manage_camera_ui, + // Audio Systems load_audio, unload_audio, - spawn_scene, play_audio, + // Level Import/Export systems export_level, import_level, + // Misc/Debug Systems load_bogus, ), ) .run(); } +/// A generic referenece used for UI elements to point to assets or entities +#[derive(Debug, Component)] +enum UiRef { + Handle(T), + Entity(T), +} + /// UI: /// * GLTFs /// * Scenes @@ -62,16 +97,36 @@ fn initialize_ui(mut commands: Commands) { ..default() }) .with_children(|parent| { - parent.spawn((GameUiList("GLTFs"), NodeBundle { ..default() }, GltfsUi)); - parent.spawn((GameUiList("Scenes"), NodeBundle { ..default() }, ScenesUi)); - parent.spawn((GameUiList("Cameras"), NodeBundle { ..default() }, CamerasUi)); - parent.spawn(( - GameUiList("Animations"), - NodeBundle { ..default() }, - AnimationsUi, - )); + parent + .spawn(( + GameUiList, + Name::new("GLTFs"), + NodeBundle { ..default() }, + GltfsUi, + )) + .with_children(|parent| { + parent.spawn(( + GameUiList, + Name::new("Scenes"), + NodeBundle { ..default() }, + ScenesUi, + )); + parent.spawn(( + GameUiList, + Name::new("Cameras"), + NodeBundle { ..default() }, + CamerasUi, + )); + parent.spawn(( + GameUiList, + Name::new("Animations"), + NodeBundle { ..default() }, + AnimationsUi, + )); + }); parent.spawn(( - GameUiSet("Audio Clips"), + GameUiSet, + Name::new("Audio Clips"), NodeBundle { ..default() }, AudioClipsUi, )); @@ -92,32 +147,195 @@ fn load_bogus( ) .for_each(|_| { commands - .spawn((GameUiButton("bogus"), NodeBundle { ..default() })) + .spawn((GameUiButton, Name::new("bogus"), NodeBundle { ..default() })) .set_parent(root.single()); }) } +#[derive(Resource, Default, Debug)] +struct AssetRegistry(HashSet); + /// Component marking UI for loaded Gltf assets #[derive(Component)] struct GltfsUi; +/// Drag+Drop import GLTF to editor +fn load_gltf( + mut events: EventReader, + server: Res, + mut assets: ResMut, +) { + events + .iter() + .filter_map(|event| match event { + FileDragAndDrop::DroppedFile { path_buf, .. } => Some(path_buf), + _ => None, + }) + .for_each(|path_buf| { + let path = path_buf.as_path(); + let handle = server.load_untyped(path); + assets.0.insert(handle); + }); +} + +/// Helper method to extract the stripped filename given a full asset path +fn get_fname(asset_path: AssetPath, suffixes: &[&str]) -> String { + let path = asset_path.path().file_name().expect("Filename"); + let path_str = path.to_str().expect("Asset Path to Str"); + let name_str = suffixes + .iter() + .rfold(path_str, |acc, &suffix| acc.trim_end_matches(suffix)); + String::from(name_str) +} + +/// Sync GLTF assets with UI +/// +/// TODO: Handle failed load events +/// Options: +/// * Show Error message, do not add to UI +/// * Add to UI with visual indicator +/// This should be a separate async system +fn manage_gltf_ui( + mut events: EventReader>, + root: Query>, + mut commands: Commands, + server: Res, +) { + events + .iter() + .filter_map(|event| match event { + AssetEvent::Created { handle } => { + let asset_path = server + .get_handle_path(handle.clone()) + .expect("Fetch Asset Path"); + let name = get_fname(asset_path, &[".gltf", ".glb"]); + Some((handle.clone(), String::from(name))) + } + _ => None, + }) + .for_each(|(handle, name)| { + commands + .spawn(( + GameUiButton, + Name::new(name), + NodeBundle { ..default() }, + GltfsUi, + UiRef::Handle(handle.clone()), + )) + .set_parent(root.single()); + }); +} + +/// Remove gltf from editor +fn unload_gltf() {} + /// Component marking UI for Scene assets #[derive(Component)] struct ScenesUi; +/// Sync scene assets with UI +fn manage_scene_ui( + mut events: EventReader>, + root: Query>, + mut commands: Commands, + gltfs: Res>, + registry: Res, +) { + events + .iter() + .filter_map(|event| match event { + AssetEvent::Created { handle } => { + let name = registry + .0 + .iter() + .find_map( + |gltf_handle| match gltfs.get(&gltf_handle.clone().typed::()) { + Some(gltf) => { + gltf.named_scenes.iter().find_map(|(name, scene_handle)| { + info!( + "scene_handle({:?}) == handle({:?})", + scene_handle, handle + ); + (scene_handle == handle).then_some(name) + }) + } + None => None, + }, + ) + .expect("Find scene name"); + Some((handle.clone(), String::from(name))) + } + _ => None, + }) + .for_each(|(handle, name)| { + commands + .spawn(( + GameUiButton, + Name::new(name), + NodeBundle { ..default() }, + ScenesUi, + UiRef::Handle(handle.clone()), + )) + .set_parent(root.single()); + }); +} + /// Component marking UI for Camera assets #[derive(Component)] struct CamerasUi; +fn manage_camera_ui() {} + /// Component marking UI for Animation assets #[derive(Component)] struct AnimationsUi; -/// Drag+Drop import GLTF to editor -fn load_gltf() {} - -/// Remove gltf from editor -fn unload_gltf() {} +fn manage_animation_ui( + mut events: EventReader>, + root: Query>, + mut commands: Commands, + gltfs: Res>, + registry: Res, +) { + events + .iter() + .filter_map(|event| match event { + AssetEvent::Created { handle } => { + let name = + registry + .0 + .iter() + .find_map(|gltf_handle| { + match gltfs.get(&gltf_handle.clone().typed::()) { + Some(gltf) => gltf.named_animations.iter().find_map( + |(name, animation_handle)| { + info!( + "animation_handle({:?}) == handle({:?})", + animation_handle, handle + ); + (animation_handle == handle).then_some(name) + }, + ), + None => None, + } + }) + .expect("Find animation name"); + Some((handle.clone(), String::from(name))) + } + _ => None, + }) + .for_each(|(handle, name)| { + commands + .spawn(( + GameUiButton, + Name::new(name), + NodeBundle { ..default() }, + AnimationsUi, + UiRef::Handle(handle.clone()), + )) + .set_parent(root.single()); + }); +} /// Component marking UI for Audio Clip assets #[derive(Component)] diff --git a/src/ui.rs b/src/ui.rs index bbc4717..28a138c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -18,69 +18,73 @@ impl Plugin for GameUiPlugin { /// GameUiList for holding ordered collections of objects #[derive(Debug, Component)] -pub struct GameUiList(pub &'static str); +pub struct GameUiList; /// Manage UI Lists: lists of UI entities. -fn manage_ui_list(events: Query<(Entity, &GameUiList), Added>, mut commands: Commands) { - events.iter().for_each(|(entity, ui_list)| { - info!("Expanding UI List {:?}", ui_list); - commands.entity(entity).insert(NodeBundle { - style: Style { - width: Val::Px(100.0), - margin: UiRect::all(Val::Px(2.0)), - padding: UiRect::all(Val::Px(2.0)), - border: UiRect::all(Val::Px(2.0)), - flex_direction: FlexDirection::Column, - align_items: AlignItems::Stretch, - justify_items: JustifyItems::Center, - align_content: AlignContent::FlexStart, +fn manage_ui_list(events: Query<(Entity, &Name), Added>, mut commands: Commands) { + events.iter().for_each(|(entity, name)| { + commands + .entity(entity) + .insert(NodeBundle { + style: Style { + // width: Val::Px(100.0), + margin: UiRect::all(Val::Px(2.0)), + padding: UiRect::all(Val::Px(2.0)), + border: UiRect::all(Val::Px(2.0)), + flex_direction: FlexDirection::Column, + align_items: AlignItems::Stretch, + justify_items: JustifyItems::Center, + align_content: AlignContent::FlexStart, + ..default() + }, + background_color: BackgroundColor(Color::RED), + border_color: BorderColor(Color::BLACK), ..default() - }, - background_color: BackgroundColor(Color::RED), - border_color: BorderColor(Color::BLACK), - ..default() - }); + }) + .with_children(|parent| { + parent.spawn(TextBundle::from_section(name, TextStyle { ..default() })); + }); }); } /// GameUiSet Component for holding collections of objects #[derive(Debug, Component)] -pub struct GameUiSet(pub &'static str); +pub struct GameUiSet; /// Manage UI Sets: collections of UI entities. -fn manage_ui_set(events: Query<(Entity, &GameUiSet), Added>, mut commands: Commands) { - events.iter().for_each(|(entity, ui_set)| { - info!("Expanding UI Set {:?}", ui_set); - commands.entity(entity).insert(NodeBundle { - style: Style { - width: Val::Px(100.0), - margin: UiRect::all(Val::Px(2.0)), - padding: UiRect::all(Val::Px(2.0)), - border: UiRect::all(Val::Px(2.0)), - align_items: AlignItems::FlexStart, - align_content: AlignContent::FlexStart, - flex_direction: FlexDirection::Row, - flex_wrap: FlexWrap::Wrap, +fn manage_ui_set(events: Query<(Entity, &Name), Added>, mut commands: Commands) { + events.iter().for_each(|(entity, name)| { + commands + .entity(entity) + .insert(NodeBundle { + style: Style { + // width: Val::Px(100.0), + margin: UiRect::all(Val::Px(2.0)), + padding: UiRect::all(Val::Px(2.0)), + border: UiRect::all(Val::Px(2.0)), + align_items: AlignItems::FlexStart, + align_content: AlignContent::FlexStart, + flex_direction: FlexDirection::Row, + flex_wrap: FlexWrap::Wrap, + ..default() + }, + background_color: BackgroundColor(Color::BLUE), + border_color: BorderColor(Color::BLACK), ..default() - }, - background_color: BackgroundColor(Color::BLUE), - border_color: BorderColor(Color::BLACK), - ..default() - }); + }) + .with_children(|parent| { + parent.spawn(TextBundle::from_section(name, TextStyle { ..default() })); + }); }); } /// GameUiButton for interactive elements #[derive(Debug, Component)] -pub struct GameUiButton(pub &'static str); +pub struct GameUiButton; /// Manage UI Buttons. interactive buttons. -fn manage_ui_button( - events: Query<(Entity, &GameUiButton), Added>, - mut commands: Commands, -) { - events.iter().for_each(|(entity, ui_button)| { - info!("Expanding UI Button {:?}", ui_button); +fn manage_ui_button(events: Query<(Entity, &Name), Added>, mut commands: Commands) { + events.iter().for_each(|(entity, name)| { commands .entity(entity) .insert(ButtonBundle { @@ -96,7 +100,7 @@ fn manage_ui_button( ..default() }) .with_children(|parent| { - parent.spawn(TextBundle::from_section(ui_button.0, TextStyle::default())); + parent.spawn(TextBundle::from_section(name, TextStyle::default())); }); }); }