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