Compare commits
No commits in common. 'a2c3ceade366c7a183eb47559633f81511b9fbe3' and '708c7ac54dbc31c90d53373c3aa63637ef4a6139' have entirely different histories.
a2c3ceade3
...
708c7ac54d
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,8 +0,0 @@
|
|||||||
- [x] basic gltf inspector
|
|
||||||
- [x] with animation previews
|
|
||||||
- [ ] inspect specific models
|
|
||||||
- [ ] Use gltf camera
|
|
||||||
- [x] basic text inspector
|
|
||||||
- [x] with simple text animation
|
|
||||||
- [ ] audio inspector
|
|
||||||
- [x] debug info (FPS)
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
use bevy::prelude::*;
|
|
||||||
use monologue_trees::debug::*;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
App::new()
|
|
||||||
.add_plugins((
|
|
||||||
DefaultPlugins.set(WindowPlugin {
|
|
||||||
primary_window: Some(Window {
|
|
||||||
title: "Audio Inspect".into(),
|
|
||||||
resolution: (640., 480.).into(),
|
|
||||||
..default()
|
|
||||||
}),
|
|
||||||
..default()
|
|
||||||
}),
|
|
||||||
DebugInfoPlugin,
|
|
||||||
))
|
|
||||||
.add_systems(PreStartup, (load,))
|
|
||||||
.add_systems(Startup, (init,))
|
|
||||||
.add_systems(Update, (save, update))
|
|
||||||
.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stores audio handles so they don't get dropped
|
|
||||||
#[derive(Component, Debug)]
|
|
||||||
struct AudioItem(Handle<AudioSource>);
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
struct Container;
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Load audio assets
|
|
||||||
/// TODO: Does this load new items when they're added to the folder?
|
|
||||||
fn load(server: Res<AssetServer>) {
|
|
||||||
// To prevent handles from being dropped,
|
|
||||||
// they are stored on entities created triggered on AssetEvent<AudioSource>::Created
|
|
||||||
let _yeet = server.load_folder("audio").expect("Load audios");
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Initialize audio inspector UI
|
|
||||||
fn init(mut commands: Commands) {
|
|
||||||
commands.spawn(Camera3dBundle { ..default() });
|
|
||||||
|
|
||||||
commands
|
|
||||||
.spawn((
|
|
||||||
NodeBundle {
|
|
||||||
style: Style {
|
|
||||||
width: Val::Percent(100.0),
|
|
||||||
height: Val::Percent(100.0),
|
|
||||||
align_items: AlignItems::FlexStart,
|
|
||||||
flex_direction: FlexDirection::Column,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
background_color: BackgroundColor(Color::MIDNIGHT_BLUE),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
Container,
|
|
||||||
))
|
|
||||||
.with_children(|parent| {
|
|
||||||
// Something might go here...
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Save loaded audio sources to entities
|
|
||||||
fn save(
|
|
||||||
mut audio_load_events: EventReader<AssetEvent<AudioSource>>,
|
|
||||||
server: Res<AssetServer>,
|
|
||||||
mut commands: Commands,
|
|
||||||
container_q: Query<Entity, With<Container>>,
|
|
||||||
) {
|
|
||||||
for event in audio_load_events.iter() {
|
|
||||||
match event {
|
|
||||||
AssetEvent::Created { handle } => {
|
|
||||||
let style = TextStyle {
|
|
||||||
color: Color::BLACK,
|
|
||||||
font_size: 16.0,
|
|
||||||
..default()
|
|
||||||
};
|
|
||||||
let handle_path = server
|
|
||||||
.get_handle_path(handle.clone())
|
|
||||||
.expect("Get handle path");
|
|
||||||
let path = handle_path.path().to_str().expect("Convert path to str");
|
|
||||||
let title = path
|
|
||||||
.split("/")
|
|
||||||
.last()
|
|
||||||
.expect("Extracting filename")
|
|
||||||
.trim_end_matches(".ogg");
|
|
||||||
|
|
||||||
commands
|
|
||||||
.spawn((
|
|
||||||
ButtonBundle {
|
|
||||||
style: Style { ..default() },
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
AudioItem(handle.clone()),
|
|
||||||
))
|
|
||||||
.with_children(|parent| {
|
|
||||||
parent.spawn(TextBundle::from_section(title, style));
|
|
||||||
})
|
|
||||||
.set_parent(container_q.single());
|
|
||||||
}
|
|
||||||
AssetEvent::Modified { .. } => {
|
|
||||||
debug_assert!(false, "Audio file was modified, not handled!")
|
|
||||||
}
|
|
||||||
AssetEvent::Removed { .. } => {
|
|
||||||
debug_assert!(false, "Audio file was deleted, not handled!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update loop; play/pause/volume
|
|
||||||
fn update() {}
|
|
||||||
@ -1,498 +1,566 @@
|
|||||||
use bevy::{gltf::Gltf, prelude::*, utils::HashMap};
|
use bevy::{
|
||||||
use monologue_trees::debug::*;
|
core_pipeline::clear_color::ClearColorConfig,
|
||||||
|
gltf::Gltf,
|
||||||
|
input::{
|
||||||
|
keyboard::KeyboardInput,
|
||||||
|
mouse::{MouseMotion, MouseWheel},
|
||||||
|
ButtonState,
|
||||||
|
},
|
||||||
|
pbr::CascadeShadowConfigBuilder,
|
||||||
|
prelude::*,
|
||||||
|
render::{
|
||||||
|
camera::RenderTarget,
|
||||||
|
render_resource::{
|
||||||
|
Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
utils::HashSet,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugins((
|
.add_plugins(DefaultPlugins.set(WindowPlugin {
|
||||||
DefaultPlugins.set(WindowPlugin {
|
|
||||||
primary_window: Some(Window {
|
primary_window: Some(Window {
|
||||||
title: "GLTF Inspector".into(),
|
title: "GLTF Inspector".into(),
|
||||||
resolution: (640., 480.).into(),
|
resolution: (640., 480.).into(),
|
||||||
..default()
|
..default()
|
||||||
}),
|
}),
|
||||||
..default()
|
..default()
|
||||||
}),
|
}))
|
||||||
DebugInfoPlugin,
|
.add_event::<ManageActive>()
|
||||||
))
|
.add_startup_system(load_models)
|
||||||
.add_event::<ResetScene>()
|
.add_startup_system(spawn_base_scene)
|
||||||
.add_event::<SpawnScene>()
|
.add_startup_system(spawn_base_ui)
|
||||||
.init_resource::<Current>()
|
.add_system(spawn_models)
|
||||||
.add_systems(Startup, spawn_ui)
|
.add_system(spawn_ui)
|
||||||
.add_systems(PreUpdate, (add_camera_ui, add_scene_ui, add_animation_ui))
|
.add_system(control_animation)
|
||||||
.add_systems(
|
.add_system(rotate_model)
|
||||||
Update,
|
.add_system(zoom_model)
|
||||||
(
|
.add_system(scroll)
|
||||||
drag_and_drop,
|
.add_system(select)
|
||||||
loading,
|
.add_system(manage_active)
|
||||||
reset_scene.before(spawn_scene),
|
|
||||||
spawn_scene.after(reset_scene),
|
|
||||||
control_scene,
|
|
||||||
control_animation,
|
|
||||||
control_camera,
|
|
||||||
control_default_camera,
|
|
||||||
control_default_light,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.add_systems(
|
|
||||||
PostUpdate,
|
|
||||||
(
|
|
||||||
clean_ui::<SceneButton>,
|
|
||||||
clean_ui::<AnimationButton>,
|
|
||||||
clean_ui::<CameraButton>,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
///
|
||||||
struct SceneMarker;
|
/// Stores GLTF handles for later use
|
||||||
|
#[derive(Resource)]
|
||||||
#[derive(Component)]
|
struct Models {
|
||||||
struct DefaultCamera;
|
handles: Vec<Handle<Gltf>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Marks GLTF model as inspectable
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct DefaultLight;
|
struct Inspect;
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct MainUi;
|
struct SelectionUI;
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct UiTitle;
|
struct PreviewCamera;
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct InstructionsUi;
|
struct ScrollingList;
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct SceneSelectUi;
|
struct Preview(Handle<Image>);
|
||||||
|
|
||||||
#[derive(Component, PartialEq, Debug)]
|
|
||||||
struct SceneButton(Handle<Scene>);
|
|
||||||
|
|
||||||
#[derive(Event)]
|
|
||||||
struct SpawnScene(Handle<Scene>);
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
struct AnimationSelectUi;
|
|
||||||
|
|
||||||
#[derive(Component, PartialEq, Debug)]
|
|
||||||
struct AnimationButton(Handle<AnimationClip>);
|
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct CameraSelectUi;
|
struct Container;
|
||||||
|
|
||||||
#[derive(Component, PartialEq, Debug)]
|
|
||||||
struct CameraButton(Entity);
|
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
|
||||||
struct Current {
|
|
||||||
gltf: Handle<Gltf>,
|
|
||||||
scenes: HashMap<String, Handle<Scene>>,
|
|
||||||
animations: HashMap<String, Handle<AnimationClip>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Event)]
|
|
||||||
struct ResetScene;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct ResetAnimation;
|
struct Active;
|
||||||
|
|
||||||
fn drag_and_drop(
|
///
|
||||||
mut events: EventReader<FileDragAndDrop>,
|
/// Event for managing which entities are tagged "Active"
|
||||||
server: Res<AssetServer>,
|
#[derive(Debug)]
|
||||||
mut current: ResMut<Current>,
|
struct ManageActive(Option<Entity>);
|
||||||
) {
|
|
||||||
events
|
///
|
||||||
|
/// Load all GLTF models on startup
|
||||||
|
fn load_models(mut commands: Commands, ass: Res<AssetServer>) {
|
||||||
|
let weak_handles = ass.load_folder("models").expect("Load gltfs");
|
||||||
|
let handles: Vec<Handle<Gltf>> = weak_handles
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|event| match event {
|
.map(|weak| weak.clone().typed::<Gltf>())
|
||||||
FileDragAndDrop::DroppedFile { path_buf, .. } => Some(path_buf),
|
.collect();
|
||||||
_ => None,
|
// info!("Scene Handles: {:#?}", handles);
|
||||||
})
|
commands.insert_resource(Models { handles });
|
||||||
.map(|path_buf| server.load(path_buf.to_str().expect("PathBuf to str")))
|
|
||||||
.for_each(|handle| current.gltf = handle.clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_ui(mut commands: Commands) {
|
///
|
||||||
// TODO: Warn no camera (hidden)
|
/// Spawn base scene
|
||||||
// TODO: Scene select container
|
fn spawn_base_scene(mut commands: Commands) {
|
||||||
// TODO: Animation Play/Pause Placeholder
|
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
Camera3dBundle {
|
Camera2dBundle {
|
||||||
transform: Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
|
camera_2d: Camera2d {
|
||||||
..default()
|
clear_color: ClearColorConfig::Custom(Color::BLACK),
|
||||||
},
|
},
|
||||||
UiCameraConfig { show_ui: true },
|
|
||||||
DefaultCamera,
|
|
||||||
));
|
|
||||||
commands.spawn((
|
|
||||||
DirectionalLightBundle {
|
|
||||||
transform: Transform::default().looking_at(Vec3::new(1.0, -1.0, -1.0), Vec3::Y),
|
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
DefaultLight,
|
UiCameraConfig { ..default() },
|
||||||
|
SelectionUI,
|
||||||
));
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_base_ui(mut commands: Commands) {
|
||||||
commands
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
NodeBundle {
|
NodeBundle {
|
||||||
style: Style {
|
style: Style {
|
||||||
flex_wrap: FlexWrap::Wrap,
|
justify_content: JustifyContent::Center,
|
||||||
flex_direction: FlexDirection::Row,
|
size: Size::all(Val::Percent(90.0)),
|
||||||
justify_content: JustifyContent::SpaceAround,
|
overflow: Overflow::Hidden,
|
||||||
width: Val::Percent(100.0),
|
|
||||||
height: Val::Percent(100.0),
|
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
MainUi,
|
SelectionUI,
|
||||||
))
|
))
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
parent
|
parent.spawn((
|
||||||
.spawn(NodeBundle {
|
NodeBundle {
|
||||||
style: Style {
|
style: Style {
|
||||||
flex_direction: FlexDirection::Column,
|
flex_wrap: FlexWrap::Wrap,
|
||||||
|
flex_direction: FlexDirection::Row,
|
||||||
|
justify_content: JustifyContent::SpaceAround,
|
||||||
|
size: Size::AUTO,
|
||||||
|
max_size: Size::UNDEFINED,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
..default()
|
..default()
|
||||||
})
|
|
||||||
.with_children(|parent| {
|
|
||||||
parent.spawn((
|
|
||||||
TextBundle::from_section(
|
|
||||||
"Drag and Drop .gltf/.glb file",
|
|
||||||
TextStyle {
|
|
||||||
color: Color::WHITE,
|
|
||||||
..default()
|
|
||||||
},
|
},
|
||||||
),
|
ScrollingList,
|
||||||
InstructionsUi,
|
|
||||||
));
|
));
|
||||||
parent.spawn((
|
});
|
||||||
TextBundle::from_section(
|
}
|
||||||
"Using default camera",
|
|
||||||
TextStyle {
|
///
|
||||||
color: Color::WHITE,
|
/// Spawn a loaded scene for inspection
|
||||||
|
/// TODO: Update/add/delete when files are updated
|
||||||
|
fn spawn_models(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut scene_evr: EventReader<AssetEvent<Scene>>,
|
||||||
|
mut images: ResMut<Assets<Image>>,
|
||||||
|
) {
|
||||||
|
if !scene_evr.is_empty() {
|
||||||
|
info!("Spawning scenes");
|
||||||
|
}
|
||||||
|
|
||||||
|
for ev in scene_evr.iter() {
|
||||||
|
match ev {
|
||||||
|
AssetEvent::Created { handle } => {
|
||||||
|
info!("Creating scene {:#?}", handle);
|
||||||
|
|
||||||
|
// Preview image
|
||||||
|
let preview_image_handle = {
|
||||||
|
info!("Creating preview");
|
||||||
|
|
||||||
|
let size = Extent3d {
|
||||||
|
width: 256,
|
||||||
|
height: 256,
|
||||||
..default()
|
..default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create render target image for the preview camera
|
||||||
|
let mut image = Image {
|
||||||
|
texture_descriptor: TextureDescriptor {
|
||||||
|
label: None,
|
||||||
|
size,
|
||||||
|
dimension: TextureDimension::D2,
|
||||||
|
format: TextureFormat::Bgra8UnormSrgb,
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
usage: TextureUsages::TEXTURE_BINDING
|
||||||
|
| TextureUsages::COPY_DST
|
||||||
|
| TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[],
|
||||||
},
|
},
|
||||||
),
|
|
||||||
DefaultCamera,
|
|
||||||
));
|
|
||||||
parent.spawn((
|
|
||||||
TextBundle::from_section(
|
|
||||||
"Using default light",
|
|
||||||
TextStyle {
|
|
||||||
color: Color::WHITE,
|
|
||||||
..default()
|
..default()
|
||||||
},
|
};
|
||||||
),
|
|
||||||
DefaultLight,
|
// Fill with zeroes
|
||||||
));
|
image.resize(size);
|
||||||
});
|
|
||||||
parent
|
let image_handle = images.add(image);
|
||||||
|
|
||||||
|
image_handle
|
||||||
|
};
|
||||||
|
|
||||||
|
let local = {
|
||||||
|
// Get a unique number for this resource from the handle
|
||||||
|
let idx = handle.id().reflect_hash().unwrap();
|
||||||
|
// Set the origin of this scene to [idx, idx, idx];
|
||||||
|
let origin = Vec3::ONE * ((idx % 1000) as f32);
|
||||||
|
// Transform pointing at origin
|
||||||
|
Transform::from_translation(origin)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spawn the actual scene
|
||||||
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
NodeBundle {
|
SpatialBundle {
|
||||||
style: Style {
|
transform: local,
|
||||||
flex_direction: FlexDirection::Column,
|
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
|
Inspect,
|
||||||
|
Container,
|
||||||
|
Preview(preview_image_handle.clone()),
|
||||||
|
))
|
||||||
|
.with_children(|builder| {
|
||||||
|
let camera_location =
|
||||||
|
Transform::from_xyz(0.0, 0.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y);
|
||||||
|
|
||||||
|
// Spawn preview camera
|
||||||
|
builder.spawn((
|
||||||
|
Camera3dBundle {
|
||||||
|
camera_3d: Camera3d {
|
||||||
|
clear_color: ClearColorConfig::Custom(Color::WHITE),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
SceneSelectUi,
|
camera: Camera {
|
||||||
))
|
order: -1,
|
||||||
.with_children(|parent| {
|
target: RenderTarget::Image(preview_image_handle.clone()),
|
||||||
parent.spawn((
|
|
||||||
TextBundle::from_section("Scenes", TextStyle { ..default() }),
|
|
||||||
UiTitle,
|
|
||||||
));
|
|
||||||
});
|
|
||||||
parent
|
|
||||||
.spawn((
|
|
||||||
NodeBundle {
|
|
||||||
style: Style {
|
|
||||||
flex_direction: FlexDirection::Column,
|
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
|
transform: camera_location,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
CameraSelectUi,
|
UiCameraConfig { show_ui: false },
|
||||||
))
|
PreviewCamera,
|
||||||
.with_children(|parent| {
|
|
||||||
parent.spawn((
|
|
||||||
TextBundle::from_section("Cameras", TextStyle { ..default() }),
|
|
||||||
UiTitle,
|
|
||||||
));
|
));
|
||||||
});
|
|
||||||
parent
|
// Spawn window camera
|
||||||
.spawn((
|
builder.spawn((
|
||||||
NodeBundle {
|
Camera3dBundle {
|
||||||
style: Style {
|
camera_3d: Camera3d {
|
||||||
flex_direction: FlexDirection::Column,
|
clear_color: ClearColorConfig::Custom(Color::WHITE),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
|
camera: Camera {
|
||||||
|
is_active: false,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
AnimationSelectUi,
|
transform: camera_location,
|
||||||
))
|
..default()
|
||||||
.with_children(|parent| {
|
},
|
||||||
parent.spawn((
|
Inspect,
|
||||||
TextBundle::from_section("Animations", TextStyle { ..default() }),
|
Preview(preview_image_handle.clone()),
|
||||||
UiTitle,
|
|
||||||
));
|
));
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When a new camera is loaded, clear the camera buttons
|
|
||||||
fn clean_ui<T: Component>(
|
|
||||||
mut spawn_events: EventReader<SpawnScene>,
|
|
||||||
mut clear_events: EventReader<ResetScene>,
|
|
||||||
cameras: Query<Entity, With<T>>,
|
|
||||||
mut commands: Commands,
|
|
||||||
) {
|
|
||||||
// We don't care about the content of these events, just that we want to clear the UI after
|
|
||||||
// each spawn or reset event, so we map each event to () and chain the two event streams.
|
|
||||||
spawn_events
|
|
||||||
.iter()
|
|
||||||
.map(|_| ())
|
|
||||||
.chain(clear_events.iter().map(|_| ()))
|
|
||||||
.for_each(|_| {
|
|
||||||
cameras.iter().for_each(|entity| {
|
|
||||||
commands.entity(entity).despawn_recursive();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Translate UI button presses into scene spawn events
|
builder.spawn((
|
||||||
fn control_scene(
|
DirectionalLightBundle {
|
||||||
mut events: EventWriter<SpawnScene>,
|
directional_light: DirectionalLight {
|
||||||
interactions: Query<(&Interaction, &SceneButton), (Changed<Interaction>, With<Button>)>,
|
shadows_enabled: true,
|
||||||
) {
|
..default()
|
||||||
// Handle UI buttons
|
},
|
||||||
interactions
|
cascade_shadow_config: CascadeShadowConfigBuilder {
|
||||||
.iter()
|
num_cascades: 1,
|
||||||
.for_each(|(interaction, SceneButton(handle))| match interaction {
|
maximum_distance: 1.6,
|
||||||
Interaction::Pressed => events.send(SpawnScene(handle.clone())),
|
..default()
|
||||||
_ => (),
|
}
|
||||||
});
|
.into(),
|
||||||
}
|
..default()
|
||||||
|
},
|
||||||
|
Inspect,
|
||||||
|
));
|
||||||
|
|
||||||
/// Add scene buttons when a new scene is added/gltf is loaded
|
builder.spawn((
|
||||||
fn add_scene_ui(
|
SceneBundle {
|
||||||
mut events: EventReader<SpawnScene>,
|
scene: handle.clone(),
|
||||||
current: Res<Current>,
|
|
||||||
root: Query<Entity, With<SceneSelectUi>>,
|
|
||||||
mut commands: Commands,
|
|
||||||
) {
|
|
||||||
events.iter().for_each(|_| {
|
|
||||||
commands.entity(root.single()).with_children(|parent| {
|
|
||||||
current.scenes.iter().for_each(|(name, handle)| {
|
|
||||||
parent
|
|
||||||
.spawn((
|
|
||||||
ButtonBundle {
|
|
||||||
background_color: BackgroundColor(Color::NONE),
|
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
SceneButton(handle.clone()),
|
Inspect,
|
||||||
))
|
));
|
||||||
.with_children(|parent| {
|
|
||||||
parent.spawn(TextBundle::from_section(name, TextStyle { ..default() }));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
AssetEvent::Removed { .. } => {
|
||||||
|
todo!("Remove deleted scene")
|
||||||
|
}
|
||||||
|
AssetEvent::Modified { .. } => {
|
||||||
|
todo!("Update modified scene")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn control_animation(
|
fn spawn_ui(
|
||||||
interactions: Query<(&Interaction, &AnimationButton), (Changed<Interaction>, With<Button>)>,
|
|
||||||
mut players: Query<(&mut AnimationPlayer, &Name)>,
|
|
||||||
clips: Res<Assets<AnimationClip>>,
|
|
||||||
) {
|
|
||||||
// For each button interaction
|
|
||||||
interactions
|
|
||||||
.iter()
|
|
||||||
.for_each(|(interaction, AnimationButton(handle))| match interaction {
|
|
||||||
// If this is a butotn press
|
|
||||||
Interaction::Pressed => players
|
|
||||||
.iter_mut()
|
|
||||||
// Find all entities compatible with this animation
|
|
||||||
.filter(|(_, name)| {
|
|
||||||
clips
|
|
||||||
.get(handle)
|
|
||||||
.expect("Check animation clip compatability")
|
|
||||||
.compatible_with(name)
|
|
||||||
})
|
|
||||||
// Play the given (checked compatible) animation
|
|
||||||
.for_each(|(mut player, _)| {
|
|
||||||
player.play(handle.clone()).repeat();
|
|
||||||
}),
|
|
||||||
_ => (),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add animation buttons when a new animation is added/gltf is loaded
|
|
||||||
fn add_animation_ui(
|
|
||||||
mut events: EventReader<SpawnScene>,
|
|
||||||
current: Res<Current>,
|
|
||||||
root: Query<Entity, With<AnimationSelectUi>>,
|
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
|
query: Query<Entity, With<ScrollingList>>,
|
||||||
|
previews: Query<&Preview, (Added<Preview>, Without<Camera>, Without<Interaction>)>,
|
||||||
) {
|
) {
|
||||||
events.iter().for_each(|_| {
|
// UI container
|
||||||
commands.entity(root.single()).with_children(|parent| {
|
if let Ok(scrolling_list_container) = query.get_single() {
|
||||||
current.animations.iter().for_each(|(name, handle)| {
|
let mut entity_commands = commands.entity(scrolling_list_container);
|
||||||
|
entity_commands.with_children(|parent| {
|
||||||
|
for Preview(image_handle) in previews.iter() {
|
||||||
|
// Preview Image
|
||||||
|
info!("Spawning image preview");
|
||||||
parent
|
parent
|
||||||
.spawn((
|
.spawn((
|
||||||
ButtonBundle {
|
ButtonBundle { ..default() },
|
||||||
background_color: BackgroundColor(Color::NONE),
|
SelectionUI,
|
||||||
..default()
|
Preview(image_handle.clone()),
|
||||||
},
|
|
||||||
AnimationButton(handle.clone()),
|
|
||||||
))
|
))
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
parent.spawn(TextBundle::from_section(name, TextStyle { ..default() }));
|
parent.spawn(ImageBundle {
|
||||||
});
|
style: Style {
|
||||||
|
size: Size::all(Val::Px(256.0)),
|
||||||
|
padding: UiRect::all(Val::Px(5.0)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
image: UiImage {
|
||||||
|
texture: image_handle.clone(),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn control_camera(
|
fn control_animation(
|
||||||
interactions: Query<(&Interaction, &CameraButton), (Changed<Interaction>, With<Button>)>,
|
mut key_evr: EventReader<KeyboardInput>,
|
||||||
mut cameras: Query<(Entity, &mut Camera), Without<DefaultCamera>>,
|
mut active_evr: EventReader<ManageActive>,
|
||||||
|
mut players: Query<&mut AnimationPlayer>,
|
||||||
|
children: Query<&Children>,
|
||||||
|
models: Res<Models>,
|
||||||
|
gltfs: Res<Assets<Gltf>>,
|
||||||
|
mut active: Local<Vec<Entity>>,
|
||||||
|
mut playing: Local<bool>,
|
||||||
|
mut dirty: Local<bool>,
|
||||||
) {
|
) {
|
||||||
interactions
|
for event in active_evr.iter() {
|
||||||
.iter()
|
match event {
|
||||||
.for_each(|(interaction, CameraButton(entity))| match interaction {
|
ManageActive(None) => {
|
||||||
Interaction::Pressed => cameras
|
// world state must be updated
|
||||||
.iter_mut()
|
*dirty = true;
|
||||||
.for_each(|(e, mut camera)| camera.is_active = e == *entity),
|
// Stop playing
|
||||||
|
*playing = false;
|
||||||
|
// Add this to a list of acive entities
|
||||||
|
(*active).clear();
|
||||||
|
}
|
||||||
|
ManageActive(Some(entity)) => {
|
||||||
|
// world state must be updated
|
||||||
|
*dirty = true;
|
||||||
|
// Start playing
|
||||||
|
*playing = true;
|
||||||
|
// Add this to a list of acive entities
|
||||||
|
(*active).push(*entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for event in key_evr.iter() {
|
||||||
|
match event {
|
||||||
|
KeyboardInput {
|
||||||
|
key_code: Some(KeyCode::Space),
|
||||||
|
state: ButtonState::Pressed,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// World state needs to be updated to deisred state
|
||||||
|
*dirty = true;
|
||||||
|
// Toggle playing
|
||||||
|
*playing = !(*playing);
|
||||||
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If there are no other cameras, use default camera
|
|
||||||
fn control_default_camera(
|
|
||||||
mut main_camera: Query<&mut Camera, With<DefaultCamera>>,
|
|
||||||
other_cameras: Query<Entity, (With<Camera>, Without<DefaultCamera>)>,
|
|
||||||
mut text_indicators: Query<&mut Visibility, With<DefaultCamera>>,
|
|
||||||
) {
|
|
||||||
// Figure out if we should use default camera
|
|
||||||
let state = other_cameras.is_empty();
|
|
||||||
|
|
||||||
// Toggle camera
|
|
||||||
main_camera
|
|
||||||
.iter_mut()
|
|
||||||
.for_each(|mut cam| cam.is_active = state);
|
|
||||||
|
|
||||||
// Update UI indicator
|
|
||||||
text_indicators.iter_mut().for_each(|mut vis| {
|
|
||||||
*vis = match state {
|
|
||||||
true => Visibility::Visible,
|
|
||||||
false => Visibility::Hidden,
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
// If world needs to be updated
|
||||||
|
if *dirty {
|
||||||
|
if *playing {
|
||||||
|
for entity in active.iter() {
|
||||||
|
for child in children.iter_descendants(*entity) {
|
||||||
|
if let Ok(mut player) = players.get_mut(child) {
|
||||||
|
for gltf_handle in models.handles.iter() {
|
||||||
|
if let Some(gltf) = gltfs.get(gltf_handle) {
|
||||||
|
for animation_handle in gltf.animations.iter() {
|
||||||
|
player.start(animation_handle.clone()).repeat();
|
||||||
|
player.resume();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("Failed to get GLTF handle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for mut player in players.iter_mut() {
|
||||||
|
player.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Done making updates
|
||||||
|
*dirty = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If there are no other lights, use default light
|
///
|
||||||
fn control_default_light(
|
/// Rotate a model as part of inspection
|
||||||
mut toggle: Query<&mut Visibility, With<DefaultLight>>,
|
/// TODO: move light with model (want to see shadows)
|
||||||
other_lights: Query<
|
fn rotate_model(
|
||||||
Entity,
|
buttons: Res<Input<MouseButton>>,
|
||||||
(
|
mut mouse_evr: EventReader<MouseMotion>,
|
||||||
Or<(With<SpotLight>, With<DirectionalLight>, With<PointLight>)>,
|
mut transforms: Query<&mut Transform, (With<Inspect>, With<Active>, Without<Camera>)>,
|
||||||
Without<DefaultLight>,
|
|
||||||
),
|
|
||||||
>,
|
|
||||||
) {
|
) {
|
||||||
toggle.iter_mut().for_each(|mut vis| {
|
if buttons.pressed(MouseButton::Left) {
|
||||||
*vis = if other_lights.is_empty() {
|
for MouseMotion { delta } in mouse_evr.iter() {
|
||||||
Visibility::Visible
|
for mut transform in transforms.iter_mut() {
|
||||||
} else {
|
let rot_y = delta.x / 1000.0;
|
||||||
Visibility::Hidden
|
let rot_x = delta.y / 1000.0;
|
||||||
|
transform.rotate_y(rot_y);
|
||||||
|
transform.rotate_x(rot_x);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add camera buttons when a new camera is added/gltf is loaded
|
///
|
||||||
fn add_camera_ui(
|
/// Zoom in and out of the model
|
||||||
events: Query<(Entity, &Name), Added<Camera>>,
|
/// TODO: Only modify selected entities
|
||||||
ui_root: Query<Entity, With<CameraSelectUi>>,
|
fn zoom_model(
|
||||||
mut commands: Commands,
|
keys: Res<Input<KeyCode>>,
|
||||||
|
mut wheel_evr: EventReader<MouseWheel>,
|
||||||
|
mut transforms: Query<&mut Transform, (With<Inspect>, With<Active>, Without<Camera>)>,
|
||||||
) {
|
) {
|
||||||
events.iter().for_each(|(entity, name)| {
|
if keys.pressed(KeyCode::LShift) {
|
||||||
commands.entity(ui_root.single()).with_children(|parent| {
|
for ev in wheel_evr.iter() {
|
||||||
parent
|
for mut transform in transforms.iter_mut() {
|
||||||
.spawn((
|
let scale = (Vec3::ONE * ev.y) / 100.0;
|
||||||
ButtonBundle {
|
transform.scale += scale;
|
||||||
background_color: BackgroundColor(Color::NONE),
|
}
|
||||||
..default()
|
}
|
||||||
},
|
}
|
||||||
CameraButton(entity),
|
|
||||||
))
|
|
||||||
.with_children(|parent| {
|
|
||||||
parent.spawn(TextBundle::from_section(name, TextStyle { ..default() }));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load gltfs and spawn default scene
|
fn scroll(
|
||||||
fn loading(
|
mut scroll_evr: EventReader<MouseWheel>,
|
||||||
mut events: EventReader<AssetEvent<Gltf>>,
|
mut query: Query<&mut Style, With<ScrollingList>>,
|
||||||
gltfs: Res<Assets<Gltf>>,
|
active: Query<Entity, With<Active>>,
|
||||||
mut spawn: EventWriter<SpawnScene>,
|
|
||||||
mut current: ResMut<Current>,
|
|
||||||
) {
|
) {
|
||||||
events.iter().for_each(|event| {
|
for ev in scroll_evr.iter() {
|
||||||
match event {
|
// Only scroll if scene not selected
|
||||||
AssetEvent::Created { handle } | AssetEvent::Modified { handle } => {
|
if active.is_empty() {
|
||||||
let gltf = gltfs.get(handle).expect("Loaded Gltf");
|
for mut s in query.iter_mut() {
|
||||||
|
s.position.top = match s.position.top {
|
||||||
// Save active scenes
|
Val::Px(current) => Val::Px(current + (ev.y * 5.0)),
|
||||||
current.scenes = gltf.named_scenes.clone();
|
_ => Val::Px(0.0),
|
||||||
current.animations = gltf.named_animations.clone();
|
};
|
||||||
|
}
|
||||||
// Despawn existing scene
|
|
||||||
let default_scene = gltf.default_scene.clone().expect("Default scene");
|
|
||||||
spawn.send(SpawnScene(default_scene));
|
|
||||||
}
|
}
|
||||||
AssetEvent::Removed { .. } => warn!("Ignoring asset removal"),
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset the current scene
|
///
|
||||||
fn reset_scene(
|
/// Click a UI element to select
|
||||||
mut events: EventReader<ResetScene>,
|
///
|
||||||
mut commands: Commands,
|
/// This is a really ugly implementation. I'm not really happy with how we have to navigate the ECS
|
||||||
current: Query<Entity, With<SceneMarker>>,
|
/// parent/child hierarchy. There should be a more direct way to correlate the scene with the
|
||||||
|
/// button.
|
||||||
|
fn select(
|
||||||
|
query: Query<(&Interaction, &Preview), (With<SelectionUI>, Changed<Interaction>)>,
|
||||||
|
mut selection_ui: Query<&mut Visibility, (With<SelectionUI>, Without<Parent>)>,
|
||||||
|
mut ui_camera: Query<&mut Camera, (With<SelectionUI>, Without<Inspect>)>,
|
||||||
|
mut scene_camera: Query<(Entity, &mut Camera, &Preview), (With<Inspect>, Without<SelectionUI>)>,
|
||||||
|
mut key_evr: EventReader<KeyboardInput>,
|
||||||
|
mut selected: Local<Option<Entity>>, // Active camera index
|
||||||
|
parent_search: Query<&Children>,
|
||||||
|
parents: Query<Entity, (With<Children>, Without<Parent>)>, // TODO: Constrain
|
||||||
|
mut events: EventWriter<ManageActive>,
|
||||||
) {
|
) {
|
||||||
events.iter().for_each(|_| {
|
for (interaction, selected_preview) in query.iter() {
|
||||||
info!("Reset scene");
|
if interaction == &Interaction::Clicked {
|
||||||
|
// Hide UI
|
||||||
current
|
let mut ui_vis = selection_ui.single_mut();
|
||||||
|
*ui_vis = Visibility::Hidden;
|
||||||
|
|
||||||
|
// Disable UI camera
|
||||||
|
let mut ui_cam = ui_camera.single_mut();
|
||||||
|
ui_cam.is_active = false;
|
||||||
|
|
||||||
|
// Determine selected scene
|
||||||
|
*selected = scene_camera
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|e| commands.entity(e).despawn_recursive())
|
.find(|(_, _, preview)| selected_preview.0 == preview.0)
|
||||||
|
.map(|(entity, _, _)| entity);
|
||||||
|
|
||||||
|
// Enable scene camera
|
||||||
|
let (_, mut scene_cam, _) = scene_camera
|
||||||
|
.get_mut(selected.expect("Selected scene should be set"))
|
||||||
|
.expect("Failed to get Scene camera");
|
||||||
|
scene_cam.is_active = true;
|
||||||
|
|
||||||
|
// Set relevant entities active
|
||||||
|
let message = parents.iter().find(|&parent| {
|
||||||
|
parent_search
|
||||||
|
.iter_descendants(parent)
|
||||||
|
.find(|&entity| Some(entity) == *selected)
|
||||||
|
.is_some()
|
||||||
});
|
});
|
||||||
|
events.send(ManageActive(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ev in key_evr.iter() {
|
||||||
|
match ev {
|
||||||
|
KeyboardInput {
|
||||||
|
state: ButtonState::Pressed,
|
||||||
|
key_code: Some(KeyCode::Escape),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if let Some(s) = *selected {
|
||||||
|
// Set all inactive
|
||||||
|
events.send(ManageActive(None));
|
||||||
|
|
||||||
|
// Disable scene camera
|
||||||
|
let (_, mut scene_cam, _) =
|
||||||
|
scene_camera.get_mut(s).expect("Failed to get Scene camera");
|
||||||
|
scene_cam.is_active = false;
|
||||||
|
|
||||||
|
// Enable UI camera
|
||||||
|
let mut ui_cam = ui_camera.single_mut();
|
||||||
|
ui_cam.is_active = true;
|
||||||
|
|
||||||
|
// Make UI visible
|
||||||
|
let mut ui_vis = selection_ui.single_mut();
|
||||||
|
*ui_vis = Visibility::Inherited;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn a desired scene
|
fn manage_active(
|
||||||
fn spawn_scene(
|
|
||||||
mut events: EventReader<SpawnScene>,
|
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
current: Query<Entity, With<SceneMarker>>,
|
mut events: EventReader<ManageActive>,
|
||||||
|
query: Query<&Children>,
|
||||||
|
current: Query<Entity, With<Active>>,
|
||||||
) {
|
) {
|
||||||
// Handle SpawnScene events
|
for event in events.iter() {
|
||||||
events.iter().for_each(|SpawnScene(handle)| {
|
info!("Setting active: {:?}", event);
|
||||||
info!("Reset scene (inline)");
|
match event {
|
||||||
current
|
ManageActive(None) => {
|
||||||
.iter()
|
for entity in current.iter() {
|
||||||
.for_each(|e| commands.entity(e).despawn_recursive());
|
if let Some(mut entity_commands) = commands.get_entity(entity) {
|
||||||
|
entity_commands.remove::<Active>();
|
||||||
info!("Spawning scene {:?}", handle);
|
}
|
||||||
commands.spawn((
|
}
|
||||||
SceneBundle {
|
}
|
||||||
scene: handle.clone(),
|
ManageActive(Some(entity)) => {
|
||||||
..default()
|
for child in query.iter_descendants(*entity) {
|
||||||
},
|
if let Some(mut child_commands) = commands.get_entity(child) {
|
||||||
SceneMarker,
|
// info!(
|
||||||
));
|
// "Active entity components: {:?} {:?}",
|
||||||
});
|
// child,
|
||||||
|
// names.get(child)
|
||||||
|
// );
|
||||||
|
// child_commands.log_components();
|
||||||
|
child_commands.insert(Active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,114 +0,0 @@
|
|||||||
use std::collections::VecDeque;
|
|
||||||
|
|
||||||
use bevy::{
|
|
||||||
input::{keyboard::KeyboardInput, ButtonState},
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Debug info plugin
|
|
||||||
pub struct DebugInfoPlugin;
|
|
||||||
|
|
||||||
impl Plugin for DebugInfoPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_systems(Startup, init)
|
|
||||||
.add_systems(Update, (toggle, update));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Debug UI component
|
|
||||||
#[derive(Component)]
|
|
||||||
struct DebugUi;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
struct DebugFps;
|
|
||||||
|
|
||||||
/// Debug info enabled marker
|
|
||||||
#[derive(Resource)]
|
|
||||||
struct DebugActive;
|
|
||||||
|
|
||||||
fn init(mut commands: Commands, server: Res<AssetServer>) {
|
|
||||||
commands
|
|
||||||
.spawn((
|
|
||||||
NodeBundle {
|
|
||||||
visibility: Visibility::Hidden,
|
|
||||||
style: Style {
|
|
||||||
position_type: PositionType::Absolute,
|
|
||||||
top: Val::Px(0.0),
|
|
||||||
left: Val::Px(0.0),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
background_color: BackgroundColor(Color::GRAY),
|
|
||||||
z_index: ZIndex::Global(999),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
DebugUi,
|
|
||||||
))
|
|
||||||
.with_children(|parent| {
|
|
||||||
let font: Handle<Font> = server.load("fonts/JMH Typewriter-Bold.otf");
|
|
||||||
let style = TextStyle {
|
|
||||||
font,
|
|
||||||
font_size: 16.0,
|
|
||||||
color: Color::ORANGE,
|
|
||||||
};
|
|
||||||
parent.spawn((TextBundle::from_section("FPS", style), DebugUi, DebugFps));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle(
|
|
||||||
mut commands: Commands,
|
|
||||||
mut key_evr: EventReader<KeyboardInput>,
|
|
||||||
active: Option<Res<DebugActive>>,
|
|
||||||
mut visibility: Query<&mut Visibility, With<DebugUi>>,
|
|
||||||
) {
|
|
||||||
for event in key_evr.iter() {
|
|
||||||
match event {
|
|
||||||
KeyboardInput {
|
|
||||||
key_code: Some(KeyCode::F12),
|
|
||||||
state: ButtonState::Pressed,
|
|
||||||
..
|
|
||||||
} => match active {
|
|
||||||
None => {
|
|
||||||
commands.insert_resource(DebugActive);
|
|
||||||
visibility
|
|
||||||
.par_iter_mut()
|
|
||||||
.for_each_mut(|mut vis| *vis = Visibility::Visible);
|
|
||||||
}
|
|
||||||
Some(_) => {
|
|
||||||
commands.remove_resource::<DebugActive>();
|
|
||||||
visibility
|
|
||||||
.par_iter_mut()
|
|
||||||
.for_each_mut(|mut vis| *vis = Visibility::Hidden);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(
|
|
||||||
active: Option<Res<DebugActive>>,
|
|
||||||
mut fps: Query<&mut Text, With<DebugFps>>,
|
|
||||||
time: Res<Time>,
|
|
||||||
mut buffer: Local<VecDeque<f32>>,
|
|
||||||
) {
|
|
||||||
if active.is_some() {
|
|
||||||
// Moving average window size
|
|
||||||
let buffer_size = 60;
|
|
||||||
|
|
||||||
// Calculate instantaneous FPS
|
|
||||||
let last_fps = 1.0 / time.delta().as_secs_f32();
|
|
||||||
|
|
||||||
// Add current FPS to front
|
|
||||||
(*buffer).push_back(last_fps);
|
|
||||||
if (*buffer).len() > buffer_size {
|
|
||||||
// Remove extra elements from back
|
|
||||||
let _ = (*buffer).pop_front();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate moving average FPS
|
|
||||||
let average: f32 = (*buffer).iter().sum::<f32>() / ((*buffer).len() as f32);
|
|
||||||
|
|
||||||
// Display FPS value
|
|
||||||
fps.single_mut().sections[0].value = format!("FPS: {:.3}", average);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
pub mod debug;
|
|
||||||
|
|
||||||
pub mod text;
|
|
||||||
@ -1,17 +1,14 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use monologue_trees::*;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugins(DefaultPlugins.set(WindowPlugin {
|
.add_plugins(DefaultPlugins.set(WindowPlugin {
|
||||||
primary_window: Some(Window {
|
primary_window: Some(Window {
|
||||||
title: "Debug Info".into(),
|
title: "Monologue Trees".into(),
|
||||||
resolution: (640., 480.).into(),
|
resolution: (640., 480.).into(),
|
||||||
..default()
|
..default()
|
||||||
}),
|
}),
|
||||||
..default()
|
..default()
|
||||||
}))
|
}))
|
||||||
.add_plugin(debug::DebugInfoPlugin)
|
.run();
|
||||||
.run()
|
|
||||||
}
|
}
|
||||||
@ -1,177 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Animated Text
|
|
||||||
///
|
|
||||||
/// The goal of this code is to get a rudimentary "Text Animation" system in place.
|
|
||||||
///
|
|
||||||
/// The use cases are:
|
|
||||||
/// * Typing Text; ala RPG dialogs like Pokemon
|
|
||||||
///
|
|
||||||
/// Eventually adding more would be cool, like movement animations, but those get really
|
|
||||||
/// complicated really fast...
|
|
||||||
///
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
pub struct AnimatedTextPlugin;
|
|
||||||
|
|
||||||
impl Plugin for AnimatedTextPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_systems(Update, (manage_texts, animate_texts));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Bundle, Default)]
|
|
||||||
pub struct AnimatedTextBundle {
|
|
||||||
pub text_bundle: TextBundle,
|
|
||||||
pub animated_text: AnimatedText,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Animated Text Marker
|
|
||||||
/// Use this to filter out entities managed by Text Animation systems
|
|
||||||
#[derive(Component, Default, Debug)]
|
|
||||||
pub struct AnimatedText {
|
|
||||||
animation_type: Option<TextAnimationType>,
|
|
||||||
animation_status: TextAnimationStatus,
|
|
||||||
animation_duration: Option<Duration>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Animated Text component
|
|
||||||
///
|
|
||||||
/// Handles all of the logistics of running text animation
|
|
||||||
impl AnimatedText {
|
|
||||||
pub fn new(animation_type: TextAnimationType) -> Self {
|
|
||||||
AnimatedText {
|
|
||||||
animation_type: Some(animation_type),
|
|
||||||
..default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play(&mut self) {
|
|
||||||
self.animation_status = TextAnimationStatus::Playing;
|
|
||||||
self.animation_duration = match self.animation_type {
|
|
||||||
None => None,
|
|
||||||
Some(TextAnimationType::Typing(seconds)) => Some(Duration::from_secs_f32(seconds)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stop(&mut self) {
|
|
||||||
self.animation_status = TextAnimationStatus::Stopped;
|
|
||||||
self.animation_duration = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle(&mut self) {
|
|
||||||
use TextAnimationStatus::*;
|
|
||||||
self.animation_status = match self.animation_status {
|
|
||||||
Playing => Stopped,
|
|
||||||
Stopped => Playing,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum TextAnimationType {
|
|
||||||
Typing(f32), // Typing text out for duration
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
enum TextAnimationStatus {
|
|
||||||
Playing,
|
|
||||||
#[default]
|
|
||||||
Stopped,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Manage individual text entities
|
|
||||||
///
|
|
||||||
/// Animated text entities need to conform to different shapes for functional reasons.
|
|
||||||
/// For example if each letter needs to be a different color, each TextSection must be a single
|
|
||||||
/// character rather than a word or paragraph.
|
|
||||||
///
|
|
||||||
/// Any time a text is updated we need to ensure it conforms to the needs of it's animation
|
|
||||||
///
|
|
||||||
/// FIXME: Only update according to animation type
|
|
||||||
fn manage_texts(mut texts: Query<&mut Text, (Changed<Text>, With<AnimatedText>)>) {
|
|
||||||
// Check if any Text entities are "dirty"
|
|
||||||
let dirty = texts
|
|
||||||
.iter()
|
|
||||||
.any(|text| text.sections.iter().any(|section| section.value.len() > 1));
|
|
||||||
// For each text
|
|
||||||
// For each section
|
|
||||||
// If section length > 1
|
|
||||||
// Break into length-1 strings
|
|
||||||
if dirty {
|
|
||||||
texts.iter_mut().for_each(|mut text| {
|
|
||||||
// Replace the existing sections with broken down sections
|
|
||||||
// Each Text Section is a single character
|
|
||||||
// On it's own this should not cause the text to look different
|
|
||||||
// But means we can modify each text section individually in color
|
|
||||||
text.sections = text
|
|
||||||
.sections
|
|
||||||
.iter()
|
|
||||||
.map(|section| {
|
|
||||||
section.value.chars().map(|c| TextSection {
|
|
||||||
value: c.into(),
|
|
||||||
style: section.style.clone(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
.collect();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn animate_texts(mut query: Query<(&mut Text, &mut AnimatedText)>, time: Res<Time>) {
|
|
||||||
for (mut text, mut animated_text) in query.iter_mut() {
|
|
||||||
match animated_text.animation_status {
|
|
||||||
TextAnimationStatus::Stopped => (),
|
|
||||||
TextAnimationStatus::Playing => match animated_text.animation_type {
|
|
||||||
None => (),
|
|
||||||
Some(TextAnimationType::Typing(seconds)) => {
|
|
||||||
animated_text.animation_duration = match animated_text.animation_duration {
|
|
||||||
None | Some(Duration::ZERO) => {
|
|
||||||
// Set sections to alpha 0 before animation begins
|
|
||||||
text.sections.iter_mut().for_each(|section| {
|
|
||||||
section.style.color.set_a(0.0);
|
|
||||||
});
|
|
||||||
// We have just (re)started the animation, so set the duration to full
|
|
||||||
Some(Duration::from_secs_f32(seconds))
|
|
||||||
}
|
|
||||||
// FIXME: Why can't mutate inner on animation_duration?
|
|
||||||
Some(inner) => {
|
|
||||||
{
|
|
||||||
// how far into the animation are we?
|
|
||||||
let percentage = 1.0 - (inner.as_secs_f32() / seconds);
|
|
||||||
|
|
||||||
// Find the total number of characters to be processed
|
|
||||||
// let len_total = text
|
|
||||||
// .sections
|
|
||||||
// .iter()
|
|
||||||
// .fold(0, |acc, curr| acc + curr.value.len());
|
|
||||||
// Backup version:
|
|
||||||
let len_total = text.sections.len();
|
|
||||||
|
|
||||||
// Find the farthest character into the string to show
|
|
||||||
let target = (len_total as f32 * percentage) as usize;
|
|
||||||
|
|
||||||
// Assign all segments an alpha of 0 or 1.
|
|
||||||
// TODO: Incremental updates only: Start at target, work backward
|
|
||||||
// until you get to one with alpha 1 and then break
|
|
||||||
text.sections
|
|
||||||
.iter_mut()
|
|
||||||
.take(target)
|
|
||||||
.rev()
|
|
||||||
.take_while(|section| section.style.color.a() != 1.0)
|
|
||||||
.for_each(|section| {
|
|
||||||
section.style.color.set_a(1.0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// We are continuing the animation, so decrement the remaining duration
|
|
||||||
Some(inner.saturating_sub(time.delta()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
if *duration > Duration::ZERO {
|
|
||||||
let mut text = texts.single_mut();
|
|
||||||
let total_sections = text.sections.len();
|
|
||||||
|
|
||||||
*duration = duration.saturating_sub(time.delta());
|
|
||||||
|
|
||||||
for (idx, section) in text.sections.iter_mut().enumerate() {
|
|
||||||
let ratio = ((idx + 1) as f32) / (total_sections as f32);
|
|
||||||
let cursor = 1.0 - ((*duration).as_secs_f32() / 30.0);
|
|
||||||
let alpha = if cursor > ratio { 1.0 } else { 0.0 };
|
|
||||||
section.style.color.set_a(alpha);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue