Compare commits
	
		
			No commits in common. 'e159346b8946e9bfa095dbe6a897218a7e516b68' and 'b9dc61dad1c171fc040845b2799172926a318f8f' have entirely different histories. 
		
	
	
		
			e159346b89
			...
			b9dc61dad1
		
	
		
	| @ -1,360 +0,0 @@ | ||||
| // 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::*}; | ||||
| 
 | ||||
| fn main() { | ||||
|     App::new() | ||||
|         .add_plugins(( | ||||
|             DefaultPlugins.set(WindowPlugin { | ||||
|                 primary_window: Some(Window { | ||||
|                     title: "Monologue Trees Editor".into(), | ||||
|                     resolution: (640., 480.).into(), | ||||
|                     ..default() | ||||
|                 }), | ||||
|                 ..default() | ||||
|             }), | ||||
|             DebugInfoPlugin, | ||||
|             GameUiPlugin, | ||||
|         )) | ||||
|         .init_resource::<AssetRegistry>() | ||||
|         .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, | ||||
|                 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<T> { | ||||
|     Handle(T), | ||||
|     Entity(T), | ||||
| } | ||||
| 
 | ||||
| /// UI:
 | ||||
| /// * GLTFs
 | ||||
| ///   * Scenes
 | ||||
| ///   * Cameras
 | ||||
| ///   * Animations
 | ||||
| /// * Audios
 | ||||
| fn initialize_ui(mut commands: Commands) { | ||||
|     commands.spawn(( | ||||
|         Camera2dBundle { ..default() }, | ||||
|         UiCameraConfig { show_ui: true }, | ||||
|     )); | ||||
| 
 | ||||
|     commands | ||||
|         .spawn(NodeBundle { | ||||
|             style: Style { | ||||
|                 width: Val::Percent(100.0), | ||||
|                 height: Val::Percent(100.0), | ||||
|                 ..default() | ||||
|             }, | ||||
|             ..default() | ||||
|         }) | ||||
|         .with_children(|parent| { | ||||
|             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, | ||||
|                 Name::new("Audio Clips"), | ||||
|                 NodeBundle { ..default() }, | ||||
|                 AudioClipsUi, | ||||
|             )); | ||||
|         }); | ||||
| } | ||||
| 
 | ||||
| fn load_bogus( | ||||
|     mut events: EventReader<KeyboardInput>, | ||||
|     root: Query<Entity, With<AnimationsUi>>, | ||||
|     mut commands: Commands, | ||||
| ) { | ||||
|     events | ||||
|         .iter() | ||||
|         .filter( | ||||
|             |&KeyboardInput { | ||||
|                  key_code, state, .. | ||||
|              }| *key_code == Some(KeyCode::Space) && *state == ButtonState::Pressed, | ||||
|         ) | ||||
|         .for_each(|_| { | ||||
|             commands | ||||
|                 .spawn((GameUiButton, Name::new("bogus"), NodeBundle { ..default() })) | ||||
|                 .set_parent(root.single()); | ||||
|         }) | ||||
| } | ||||
| 
 | ||||
| #[derive(Resource, Default, Debug)] | ||||
| struct AssetRegistry(HashSet<HandleUntyped>); | ||||
| 
 | ||||
| /// Component marking UI for loaded Gltf assets
 | ||||
| #[derive(Component)] | ||||
| struct GltfsUi; | ||||
| 
 | ||||
| /// Drag+Drop import GLTF to editor
 | ||||
| fn load_gltf( | ||||
|     mut events: EventReader<FileDragAndDrop>, | ||||
|     server: Res<AssetServer>, | ||||
|     mut assets: ResMut<AssetRegistry>, | ||||
| ) { | ||||
|     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<AssetEvent<Gltf>>, | ||||
|     root: Query<Entity, With<GltfsUi>>, | ||||
|     mut commands: Commands, | ||||
|     server: Res<AssetServer>, | ||||
| ) { | ||||
|     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<AssetEvent<Scene>>, | ||||
|     root: Query<Entity, With<ScenesUi>>, | ||||
|     mut commands: Commands, | ||||
|     gltfs: Res<Assets<Gltf>>, | ||||
|     registry: Res<AssetRegistry>, | ||||
| ) { | ||||
|     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::<Gltf>()) { | ||||
|                             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; | ||||
| 
 | ||||
| fn manage_animation_ui( | ||||
|     mut events: EventReader<AssetEvent<AnimationClip>>, | ||||
|     root: Query<Entity, With<AnimationsUi>>, | ||||
|     mut commands: Commands, | ||||
|     gltfs: Res<Assets<Gltf>>, | ||||
|     registry: Res<AssetRegistry>, | ||||
| ) { | ||||
|     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::<Gltf>()) { | ||||
|                                 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)] | ||||
| struct AudioClipsUi; | ||||
| 
 | ||||
| /// Drag+Drop import Audio to editor
 | ||||
| fn load_audio() {} | ||||
| 
 | ||||
| /// Remove audio from editor
 | ||||
| fn unload_audio() {} | ||||
| 
 | ||||
| /// Spawn Scene
 | ||||
| fn spawn_scene() {} | ||||
| 
 | ||||
| /// Play/Loop Audio
 | ||||
| fn play_audio() {} | ||||
| 
 | ||||
| /// Export level
 | ||||
| fn export_level() {} | ||||
| 
 | ||||
| /// Import Level
 | ||||
| fn import_level() {} | ||||
| @ -1,121 +0,0 @@ | ||||
| use bevy::{prelude::*, window::PrimaryWindow}; | ||||
| 
 | ||||
| pub struct GameUiPlugin; | ||||
| 
 | ||||
| impl Plugin for GameUiPlugin { | ||||
|     fn build(&self, app: &mut App) { | ||||
|         app.add_systems( | ||||
|             Update, | ||||
|             ( | ||||
|                 manage_ui_list, | ||||
|                 manage_ui_set, | ||||
|                 manage_ui_button, | ||||
|                 manage_cursor, | ||||
|             ), | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// GameUiList for holding ordered collections of objects
 | ||||
| #[derive(Debug, Component)] | ||||
| pub struct GameUiList; | ||||
| 
 | ||||
| /// Manage UI Lists: lists of UI entities.
 | ||||
| fn manage_ui_list(events: Query<(Entity, &Name), Added<GameUiList>>, 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() | ||||
|             }) | ||||
|             .with_children(|parent| { | ||||
|                 parent.spawn(TextBundle::from_section(name, TextStyle { ..default() })); | ||||
|             }); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| /// GameUiSet Component for holding collections of objects
 | ||||
| #[derive(Debug, Component)] | ||||
| pub struct GameUiSet; | ||||
| 
 | ||||
| /// Manage UI Sets: collections of UI entities.
 | ||||
| fn manage_ui_set(events: Query<(Entity, &Name), Added<GameUiSet>>, 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() | ||||
|             }) | ||||
|             .with_children(|parent| { | ||||
|                 parent.spawn(TextBundle::from_section(name, TextStyle { ..default() })); | ||||
|             }); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| /// GameUiButton for interactive elements
 | ||||
| #[derive(Debug, Component)] | ||||
| pub struct GameUiButton; | ||||
| 
 | ||||
| /// Manage UI Buttons. interactive buttons.
 | ||||
| fn manage_ui_button(events: Query<(Entity, &Name), Added<GameUiButton>>, mut commands: Commands) { | ||||
|     events.iter().for_each(|(entity, name)| { | ||||
|         commands | ||||
|             .entity(entity) | ||||
|             .insert(ButtonBundle { | ||||
|                 style: Style { | ||||
|                     margin: UiRect::all(Val::Px(2.0)), | ||||
|                     padding: UiRect::all(Val::Px(2.0)), | ||||
|                     border: UiRect::all(Val::Px(2.0)), | ||||
|                     justify_content: JustifyContent::Center, | ||||
|                     ..default() | ||||
|                 }, | ||||
|                 background_color: BackgroundColor(Color::GREEN), | ||||
|                 border_color: BorderColor(Color::BLACK), | ||||
|                 ..default() | ||||
|             }) | ||||
|             .with_children(|parent| { | ||||
|                 parent.spawn(TextBundle::from_section(name, TextStyle::default())); | ||||
|             }); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| /// Manage the cursor icon for better immersion
 | ||||
| fn manage_cursor( | ||||
|     mut primary_window: Query<&mut Window, With<PrimaryWindow>>, | ||||
|     events: Query<&Interaction, With<Interaction>>, | ||||
| ) { | ||||
|     events.iter().for_each(|event| { | ||||
|         let mut window = primary_window.single_mut(); | ||||
|         window.cursor.icon = match event { | ||||
|             Interaction::Pressed => CursorIcon::Grabbing, | ||||
|             Interaction::Hovered => CursorIcon::Hand, | ||||
|             Interaction::None => CursorIcon::Default, | ||||
|         } | ||||
|     }); | ||||
| } | ||||
					Loading…
					
					
				
		Reference in New Issue