Compare commits

..

10 Commits

Author SHA1 Message Date
Elijah Voigt a2c3ceade3 gltf inspector 0.2git add .; git commit 2 years ago
Elijah Voigt 3f37d0b601 working on rewriting gltf inspector, ui woes 2 years ago
Elijah Voigt 93d07f1a42 Dealing with scene import woes 2 years ago
Elijah Voigt 3545aa227c Got some semblence of item selection
I have a few options for "next steps" including:
* Figure out bounding boxes using meshes
* Move it into a plugin for re-usability
* Build scene from meshes/nodes for more flexibility
* Move on to something completely different (audio inspector)

I'm inclined to go to audio inspector and then come back to the
mesh/node scene builder.
2 years ago
Elijah Voigt d1a7a7f73d saving my place 2 years ago
Elijah Voigt 09d48951bc making progress on modular text animation systems 2 years ago
Elijah Voigt 8a5a8d39d4 Debug system, working on text animation 2 years ago
Elijah Voigt b381f0b7b7 text type animation 2 years ago
Elijah Voigt 678c0b8c0c Simple-ish text typing animation 2 years ago
Elijah Voigt 43281fbf12 saving my place; cool but slow text effect 2 years ago

5
.gitattributes vendored

@ -2,3 +2,8 @@
*.blend1 filter=lfs diff=lfs merge=lfs -text *.blend1 filter=lfs diff=lfs merge=lfs -text
*.gltf filter=lfs diff=lfs merge=lfs -text *.gltf filter=lfs diff=lfs merge=lfs -text
*.glb filter=lfs diff=lfs merge=lfs -text *.glb filter=lfs diff=lfs merge=lfs -text
*.bin filter=lfs diff=lfs merge=lfs -text
*.gltf.bak filter=lfs diff=lfs merge=lfs -text
*.glb.bak filter=lfs diff=lfs merge=lfs -text
*.bin.bak filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text

778
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -23,5 +23,23 @@ path = "bin/animation-wtf.rs"
name = "text-inspect" name = "text-inspect"
path = "bin/text-inspect.rs" path = "bin/text-inspect.rs"
[[bin]]
name = "debug-info"
path = "bin/debug-info.rs"
[[bin]]
name = "audio-inspect"
path = "bin/audio-inspect.rs"
[dependencies] [dependencies]
bevy = "0.10" bevy = "0.11"
bevy_rapier3d = "*"
# From rapier docs
[profile.dev.package.bevy_rapier3d]
opt-level = 3
# From rapier docs
[profile.release]
codegen-units = 1

@ -1,5 +1,26 @@
# Inspect Model # Exploration
- [ ] Load previews on startup. ## Inspectors
- [ ] Click to "Open" a model.
### Model Inspector
- [ ] Construct Scene from Nodes/Meshes (not auto-scene builder)
- [ ] Show debug info about selected model
- [ ] Wireframe view
- [ ] Automatic tighter bounding box for selection
### Audio Inspector
- [ ] UI for selecting sound
- [ ] Play/Pause/Volume
- [x] Load sounds
- [ ] Scrolling list of sounds
## WASM
- [ ] Build and run using model/text inspector
- https://github.com/bevyengine/bevy/blob/main/examples/README.md#wasm
## Text Inspector
- [ ] Performance improvements?

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/audio/Ambient/Lake Sound 1.ogg (Stored with Git LFS)

Binary file not shown.

BIN
assets/audio/Ambient/Lake Sound 2.ogg (Stored with Git LFS)

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.

BIN
assets/models/FlightHelmet.bin (Stored with Git LFS)

Binary file not shown.

BIN
assets/models/FlightHelmet.gltf (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

BIN
assets/models/inspect.blend (Stored with Git LFS)

Binary file not shown.

BIN
assets/models/inspect.blend1 (Stored with Git LFS)

Binary file not shown.

BIN
assets/models/inspect.glb (Stored with Git LFS)

Binary file not shown.

BIN
assets/models/materials.blend (Stored with Git LFS)

Binary file not shown.

BIN
assets/models/materials.blend1 (Stored with Git LFS)

Binary file not shown.

BIN
assets/models/materials.glb (Stored with Git LFS)

Binary file not shown.

BIN
assets/models/sphere.bin (Stored with Git LFS)

Binary file not shown.

BIN
assets/models/sphere.gltf (Stored with Git LFS)

Binary file not shown.

@ -0,0 +1,8 @@
- [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)

@ -0,0 +1,114 @@
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,14 +1,17 @@
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: "Monologue Trees".into(), title: "Debug Info".into(),
resolution: (640., 480.).into(), resolution: (640., 480.).into(),
..default() ..default()
}), }),
..default() ..default()
})) }))
.run(); .add_plugin(debug::DebugInfoPlugin)
.run()
} }

@ -1,566 +1,498 @@
use bevy::{ use bevy::{gltf::Gltf, prelude::*, utils::HashMap};
core_pipeline::clear_color::ClearColorConfig, use monologue_trees::debug::*;
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(DefaultPlugins.set(WindowPlugin { .add_plugins((
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()
})) }),
.add_event::<ManageActive>() DebugInfoPlugin,
.add_startup_system(load_models) ))
.add_startup_system(spawn_base_scene) .add_event::<ResetScene>()
.add_startup_system(spawn_base_ui) .add_event::<SpawnScene>()
.add_system(spawn_models) .init_resource::<Current>()
.add_system(spawn_ui) .add_systems(Startup, spawn_ui)
.add_system(control_animation) .add_systems(PreUpdate, (add_camera_ui, add_scene_ui, add_animation_ui))
.add_system(rotate_model) .add_systems(
.add_system(zoom_model) Update,
.add_system(scroll) (
.add_system(select) drag_and_drop,
.add_system(manage_active) loading,
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)]
/// Stores GLTF handles for later use struct SceneMarker;
#[derive(Resource)]
struct Models {
handles: Vec<Handle<Gltf>>,
}
///
/// Marks GLTF model as inspectable
#[derive(Component)] #[derive(Component)]
struct Inspect; struct DefaultCamera;
#[derive(Component)] #[derive(Component)]
struct SelectionUI; struct DefaultLight;
#[derive(Component)] #[derive(Component)]
struct PreviewCamera; struct MainUi;
#[derive(Component)] #[derive(Component)]
struct ScrollingList; struct UiTitle;
#[derive(Component)] #[derive(Component)]
struct Preview(Handle<Image>); struct InstructionsUi;
#[derive(Component)]
struct SceneSelectUi;
#[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 Container; struct CameraSelectUi;
#[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 Active; struct ResetAnimation;
/// fn drag_and_drop(
/// Event for managing which entities are tagged "Active" mut events: EventReader<FileDragAndDrop>,
#[derive(Debug)] server: Res<AssetServer>,
struct ManageActive(Option<Entity>); mut current: ResMut<Current>,
) {
/// 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()
.map(|weak| weak.clone().typed::<Gltf>()) .filter_map(|event| match event {
.collect(); FileDragAndDrop::DroppedFile { path_buf, .. } => Some(path_buf),
// info!("Scene Handles: {:#?}", handles); _ => None,
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) {
/// Spawn base scene // TODO: Warn no camera (hidden)
fn spawn_base_scene(mut commands: Commands) { // TODO: Scene select container
// TODO: Animation Play/Pause Placeholder
commands.spawn(( commands.spawn((
Camera2dBundle { Camera3dBundle {
camera_2d: Camera2d { transform: Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
clear_color: ClearColorConfig::Custom(Color::BLACK), ..default()
}, },
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()
}, },
UiCameraConfig { ..default() }, DefaultLight,
SelectionUI,
)); ));
}
fn spawn_base_ui(mut commands: Commands) {
commands commands
.spawn(( .spawn((
NodeBundle { NodeBundle {
style: Style { style: Style {
justify_content: JustifyContent::Center, flex_wrap: FlexWrap::Wrap,
size: Size::all(Val::Percent(90.0)), flex_direction: FlexDirection::Row,
overflow: Overflow::Hidden, justify_content: JustifyContent::SpaceAround,
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default() ..default()
}, },
..default() ..default()
}, },
SelectionUI, MainUi,
)) ))
.with_children(|parent| { .with_children(|parent| {
parent.spawn(( parent
NodeBundle { .spawn(NodeBundle {
style: Style { style: Style {
flex_wrap: FlexWrap::Wrap, flex_direction: FlexDirection::Column,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceAround,
size: Size::AUTO,
max_size: Size::UNDEFINED,
..default() ..default()
}, },
..default() ..default()
}, })
ScrollingList, .with_children(|parent| {
)); parent.spawn((
}); TextBundle::from_section(
} "Drag and Drop .gltf/.glb file",
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: &[],
}, },
..default() ),
}; InstructionsUi,
));
// Fill with zeroes parent.spawn((
image.resize(size); TextBundle::from_section(
"Using default camera",
let image_handle = images.add(image); TextStyle {
color: Color::WHITE,
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((
SpatialBundle {
transform: local,
..default() ..default()
}, },
Inspect, ),
Container, DefaultCamera,
Preview(preview_image_handle.clone()), ));
)) parent.spawn((
.with_children(|builder| { TextBundle::from_section(
let camera_location = "Using default light",
Transform::from_xyz(0.0, 0.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y); TextStyle {
color: Color::WHITE,
// Spawn preview camera
builder.spawn((
Camera3dBundle {
camera_3d: Camera3d {
clear_color: ClearColorConfig::Custom(Color::WHITE),
..default() ..default()
}, },
camera: Camera { ),
order: -1, DefaultLight,
target: RenderTarget::Image(preview_image_handle.clone()), ));
});
parent
.spawn((
NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
..default() ..default()
}, },
transform: camera_location,
..default() ..default()
}, },
UiCameraConfig { show_ui: false }, SceneSelectUi,
PreviewCamera, ))
.with_children(|parent| {
parent.spawn((
TextBundle::from_section("Scenes", TextStyle { ..default() }),
UiTitle,
)); ));
});
// Spawn window camera parent
builder.spawn(( .spawn((
Camera3dBundle { NodeBundle {
camera_3d: Camera3d { style: Style {
clear_color: ClearColorConfig::Custom(Color::WHITE), flex_direction: FlexDirection::Column,
..default()
},
camera: Camera {
is_active: false,
..default() ..default()
}, },
transform: camera_location,
..default() ..default()
}, },
Inspect, CameraSelectUi,
Preview(preview_image_handle.clone()), ))
.with_children(|parent| {
parent.spawn((
TextBundle::from_section("Cameras", TextStyle { ..default() }),
UiTitle,
)); ));
});
builder.spawn(( parent
DirectionalLightBundle { .spawn((
directional_light: DirectionalLight { NodeBundle {
shadows_enabled: true, style: Style {
flex_direction: FlexDirection::Column,
..default() ..default()
}, },
cascade_shadow_config: CascadeShadowConfigBuilder {
num_cascades: 1,
maximum_distance: 1.6,
..default()
}
.into(),
..default() ..default()
}, },
Inspect, AnimationSelectUi,
))
.with_children(|parent| {
parent.spawn((
TextBundle::from_section("Animations", TextStyle { ..default() }),
UiTitle,
)); ));
});
});
}
builder.spawn(( /// When a new camera is loaded, clear the camera buttons
SceneBundle { fn clean_ui<T: Component>(
scene: handle.clone(), mut spawn_events: EventReader<SpawnScene>,
..default() mut clear_events: EventReader<ResetScene>,
}, cameras: Query<Entity, With<T>>,
Inspect, 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
fn control_scene(
mut events: EventWriter<SpawnScene>,
interactions: Query<(&Interaction, &SceneButton), (Changed<Interaction>, With<Button>)>,
) {
// Handle UI buttons
interactions
.iter()
.for_each(|(interaction, SceneButton(handle))| match interaction {
Interaction::Pressed => events.send(SpawnScene(handle.clone())),
_ => (),
}); });
}
AssetEvent::Removed { .. } => {
todo!("Remove deleted scene")
}
AssetEvent::Modified { .. } => {
todo!("Update modified scene")
}
}
}
} }
fn spawn_ui( /// Add scene buttons when a new scene is added/gltf is loaded
fn add_scene_ui(
mut events: EventReader<SpawnScene>,
current: Res<Current>,
root: Query<Entity, With<SceneSelectUi>>,
mut commands: Commands, mut commands: Commands,
query: Query<Entity, With<ScrollingList>>,
previews: Query<&Preview, (Added<Preview>, Without<Camera>, Without<Interaction>)>,
) { ) {
// UI container events.iter().for_each(|_| {
if let Ok(scrolling_list_container) = query.get_single() { commands.entity(root.single()).with_children(|parent| {
let mut entity_commands = commands.entity(scrolling_list_container); current.scenes.iter().for_each(|(name, handle)| {
entity_commands.with_children(|parent| {
for Preview(image_handle) in previews.iter() {
// Preview Image
info!("Spawning image preview");
parent parent
.spawn(( .spawn((
ButtonBundle { ..default() }, ButtonBundle {
SelectionUI, background_color: BackgroundColor(Color::NONE),
Preview(image_handle.clone()),
))
.with_children(|parent| {
parent.spawn(ImageBundle {
style: Style {
size: Size::all(Val::Px(256.0)),
padding: UiRect::all(Val::Px(5.0)),
..default() ..default()
}, },
image: UiImage { SceneButton(handle.clone()),
texture: image_handle.clone(), ))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(name, TextStyle { ..default() }));
});
});
});
});
}
fn control_animation(
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,
) {
events.iter().for_each(|_| {
commands.entity(root.single()).with_children(|parent| {
current.animations.iter().for_each(|(name, handle)| {
parent
.spawn((
ButtonBundle {
background_color: BackgroundColor(Color::NONE),
..default() ..default()
}, },
..default() AnimationButton(handle.clone()),
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(name, TextStyle { ..default() }));
});
}); });
}); });
}
}); });
}
} }
fn control_animation( fn control_camera(
mut key_evr: EventReader<KeyboardInput>, interactions: Query<(&Interaction, &CameraButton), (Changed<Interaction>, With<Button>)>,
mut active_evr: EventReader<ManageActive>, mut cameras: Query<(Entity, &mut Camera), Without<DefaultCamera>>,
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>,
) { ) {
for event in active_evr.iter() { interactions
match event { .iter()
ManageActive(None) => { .for_each(|(interaction, CameraButton(entity))| match interaction {
// world state must be updated Interaction::Pressed => cameras
*dirty = true; .iter_mut()
// Stop playing .for_each(|(e, mut camera)| camera.is_active = e == *entity),
*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 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 cameras, use default camera
/// Rotate a model as part of inspection fn control_default_camera(
/// TODO: move light with model (want to see shadows) mut main_camera: Query<&mut Camera, With<DefaultCamera>>,
fn rotate_model( other_cameras: Query<Entity, (With<Camera>, Without<DefaultCamera>)>,
buttons: Res<Input<MouseButton>>, mut text_indicators: Query<&mut Visibility, With<DefaultCamera>>,
mut mouse_evr: EventReader<MouseMotion>,
mut transforms: Query<&mut Transform, (With<Inspect>, With<Active>, Without<Camera>)>,
) { ) {
if buttons.pressed(MouseButton::Left) { // Figure out if we should use default camera
for MouseMotion { delta } in mouse_evr.iter() { let state = other_cameras.is_empty();
for mut transform in transforms.iter_mut() {
let rot_y = delta.x / 1000.0; // Toggle camera
let rot_x = delta.y / 1000.0; main_camera
transform.rotate_y(rot_y); .iter_mut()
transform.rotate_x(rot_x); .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 there are no other lights, use default light
/// Zoom in and out of the model fn control_default_light(
/// TODO: Only modify selected entities mut toggle: Query<&mut Visibility, With<DefaultLight>>,
fn zoom_model( other_lights: Query<
keys: Res<Input<KeyCode>>, Entity,
mut wheel_evr: EventReader<MouseWheel>, (
mut transforms: Query<&mut Transform, (With<Inspect>, With<Active>, Without<Camera>)>, Or<(With<SpotLight>, With<DirectionalLight>, With<PointLight>)>,
Without<DefaultLight>,
),
>,
) { ) {
if keys.pressed(KeyCode::LShift) { toggle.iter_mut().for_each(|mut vis| {
for ev in wheel_evr.iter() { *vis = if other_lights.is_empty() {
for mut transform in transforms.iter_mut() { Visibility::Visible
let scale = (Vec3::ONE * ev.y) / 100.0; } else {
transform.scale += scale; Visibility::Hidden
}
}
} }
});
} }
fn scroll( /// Add camera buttons when a new camera is added/gltf is loaded
mut scroll_evr: EventReader<MouseWheel>, fn add_camera_ui(
mut query: Query<&mut Style, With<ScrollingList>>, events: Query<(Entity, &Name), Added<Camera>>,
active: Query<Entity, With<Active>>, ui_root: Query<Entity, With<CameraSelectUi>>,
mut commands: Commands,
) { ) {
for ev in scroll_evr.iter() { events.iter().for_each(|(entity, name)| {
// Only scroll if scene not selected commands.entity(ui_root.single()).with_children(|parent| {
if active.is_empty() { parent
for mut s in query.iter_mut() { .spawn((
s.position.top = match s.position.top { ButtonBundle {
Val::Px(current) => Val::Px(current + (ev.y * 5.0)), background_color: BackgroundColor(Color::NONE),
_ => Val::Px(0.0), ..default()
}; },
} CameraButton(entity),
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(name, TextStyle { ..default() }));
});
});
})
}
/// Load gltfs and spawn default scene
fn loading(
mut events: EventReader<AssetEvent<Gltf>>,
gltfs: Res<Assets<Gltf>>,
mut spawn: EventWriter<SpawnScene>,
mut current: ResMut<Current>,
) {
events.iter().for_each(|event| {
match event {
AssetEvent::Created { handle } | AssetEvent::Modified { handle } => {
let gltf = gltfs.get(handle).expect("Loaded Gltf");
// Save active scenes
current.scenes = gltf.named_scenes.clone();
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
/// Click a UI element to select fn reset_scene(
/// mut events: EventReader<ResetScene>,
/// This is a really ugly implementation. I'm not really happy with how we have to navigate the ECS mut commands: Commands,
/// parent/child hierarchy. There should be a more direct way to correlate the scene with the current: Query<Entity, With<SceneMarker>>,
/// 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>,
) { ) {
for (interaction, selected_preview) in query.iter() { events.iter().for_each(|_| {
if interaction == &Interaction::Clicked { info!("Reset scene");
// Hide UI
let mut ui_vis = selection_ui.single_mut(); current
*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()
.find(|(_, _, preview)| selected_preview.0 == preview.0) .for_each(|e| commands.entity(e).despawn_recursive())
.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;
}
}
_ => (),
}
}
} }
fn manage_active( /// Spawn a desired scene
fn spawn_scene(
mut events: EventReader<SpawnScene>,
mut commands: Commands, mut commands: Commands,
mut events: EventReader<ManageActive>, current: Query<Entity, With<SceneMarker>>,
query: Query<&Children>,
current: Query<Entity, With<Active>>,
) { ) {
for event in events.iter() { // Handle SpawnScene events
info!("Setting active: {:?}", event); events.iter().for_each(|SpawnScene(handle)| {
match event { info!("Reset scene (inline)");
ManageActive(None) => { current
for entity in current.iter() { .iter()
if let Some(mut entity_commands) = commands.get_entity(entity) { .for_each(|e| commands.entity(e).despawn_recursive());
entity_commands.remove::<Active>();
} info!("Spawning scene {:?}", handle);
} commands.spawn((
} SceneBundle {
ManageActive(Some(entity)) => { scene: handle.clone(),
for child in query.iter_descendants(*entity) { ..default()
if let Some(mut child_commands) = commands.get_entity(child) { },
// info!( SceneMarker,
// "Active entity components: {:?} {:?}", ));
// child, });
// names.get(child)
// );
// child_commands.log_components();
child_commands.insert(Active);
}
}
}
}
}
} }

@ -1,5 +1,7 @@
use bevy::prelude::*; use bevy::prelude::*;
use monologue_trees::{debug::*, text::*};
const LOREM: [&str; 5] = [ const LOREM: [&str; 5] = [
"Ullam nostrum aut amet adipisci consequuntur quisquam nemo consequatur. Vel eum et ullam ullam aperiam earum voluptas consequuntur. Blanditiis earum voluptatem voluptas animi dolorum fuga aliquam ea.\n", "Ullam nostrum aut amet adipisci consequuntur quisquam nemo consequatur. Vel eum et ullam ullam aperiam earum voluptas consequuntur. Blanditiis earum voluptatem voluptas animi dolorum fuga aliquam ea.\n",
"Velit ratione consequatur modi. Dolores quo quisquam occaecati veniam maxime totam minus et. Laudantium unde optio vel. Et cumque voluptatum dolorem. Odit tempore dolores quibusdam aspernatur vitae labore occaecati. Omnis quia tempora tenetur repellat in.\n", "Velit ratione consequatur modi. Dolores quo quisquam occaecati veniam maxime totam minus et. Laudantium unde optio vel. Et cumque voluptatum dolorem. Odit tempore dolores quibusdam aspernatur vitae labore occaecati. Omnis quia tempora tenetur repellat in.\n",
@ -10,28 +12,36 @@ const LOREM: [&str; 5] = [
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: "Text WTF".into(), title: "Text Inspect".into(),
resolution: (640., 480.).into(), resolution: (640., 480.).into(),
..default() ..default()
}), }),
..default() ..default()
})) }),
.add_startup_system(load_fonts) DebugInfoPlugin,
.add_startup_system(init_ui) AnimatedTextPlugin,
.add_system(manage_buttons) ))
.add_system(update) .add_systems(PreStartup, load_fonts)
.add_system(mouse_cursor) .add_systems(Startup, init_ui)
.add_systems(
Update,
(
manage_buttons,
manage_animation_button,
manage_fonts,
mouse_cursor,
manage_animation,
),
)
.run(); .run();
} }
#[derive(Resource)] #[derive(Resource)]
struct Fonts(Vec<Handle<Font>>); struct Fonts(Vec<Handle<Font>>);
#[derive(Component)]
struct Marker;
#[derive(Component)] #[derive(Component)]
struct PreviewText; struct PreviewText;
@ -41,6 +51,9 @@ struct ButtonShelf;
#[derive(Component)] #[derive(Component)]
struct FontButton(Handle<Font>); struct FontButton(Handle<Font>);
#[derive(Component)]
struct AnimationButton;
fn load_fonts(mut commands: Commands, server: Res<AssetServer>) { fn load_fonts(mut commands: Commands, server: Res<AssetServer>) {
let handles = server let handles = server
.load_folder("fonts") .load_folder("fonts")
@ -55,50 +68,94 @@ fn init_ui(mut commands: Commands) {
commands.spawn(Camera2dBundle { ..default() }); commands.spawn(Camera2dBundle { ..default() });
commands commands
.spawn(( .spawn(NodeBundle {
NodeBundle {
style: Style { style: Style {
size: Size::all(Val::Percent(100.0)), width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default() ..default()
}, },
background_color: BackgroundColor(Color::BLACK), background_color: BackgroundColor(Color::BLACK),
..default() ..default()
}, })
Marker,
))
.with_children(|parent| { .with_children(|parent| {
parent.spawn(( parent.spawn((
NodeBundle { NodeBundle {
style: Style { style: Style {
flex_direction: FlexDirection::Column, flex_direction: FlexDirection::Column,
align_items: AlignItems::Center, align_items: AlignItems::Center,
size: Size {
width: Val::Px(200.0), width: Val::Px(200.0),
height: Val::Undefined,
},
align_content: AlignContent::SpaceEvenly, align_content: AlignContent::SpaceEvenly,
..default() ..default()
}, },
background_color: BackgroundColor(Color::BLACK), background_color: BackgroundColor(Color::BLACK),
..default() ..default()
}, },
Marker,
ButtonShelf, ButtonShelf,
)); ));
{
parent.spawn(( parent.spawn((
TextBundle::from_sections(LOREM.iter().map(|&section| TextSection { AnimatedTextBundle {
value: section.into(), text_bundle: TextBundle {
text: Text {
sections: LOREM
.iter()
.map(|&line| TextSection {
value: line.into(),
style: TextStyle { style: TextStyle {
font_size: 50.0, font_size: 18.0,
color: Color::WHITE,
..default() ..default()
}, },
})), })
Marker, .collect(),
..default()
},
style: Style {
width: Val::Px(400.0),
..default()
},
..default()
},
animated_text: AnimatedText::new(TextAnimationType::Typing(12.0)),
},
PreviewText, PreviewText,
)); ));
}
parent
.spawn((
ButtonBundle {
style: Style {
align_self: AlignSelf::FlexEnd,
position_type: PositionType::Absolute,
bottom: Val::Px(5.0),
left: Val::Px(5.0),
width: Val::Px(200.0),
..default()
},
background_color: BackgroundColor(Color::default().with_a(0.0)),
..default()
},
AnimationButton,
))
.with_children(|parent| {
parent.spawn((
TextBundle {
text: Text {
sections: vec![TextSection {
value: "Toggle Animation".into(),
style: TextStyle {
font_size: 12.0,
color: Color::WHITE,
..default()
},
}],
..default()
},
..default()
},
AnimationButton,
));
});
}); });
} }
@ -107,10 +164,10 @@ fn manage_buttons(
mut commands: Commands, mut commands: Commands,
fonts: Res<Fonts>, fonts: Res<Fonts>,
server: Res<AssetServer>, server: Res<AssetServer>,
query: Query<Entity, (With<Marker>, With<ButtonShelf>)>, shelf_query: Query<Entity, With<ButtonShelf>>,
) { ) {
if fonts.is_added() || fonts.is_changed() { if fonts.is_added() || fonts.is_changed() {
let root = query.get_single().expect("Fetching root UI node"); let root = shelf_query.get_single().expect("Fetching root UI node");
let mut root_cmd = commands.get_entity(root).expect("Root UI node commands"); let mut root_cmd = commands.get_entity(root).expect("Root UI node commands");
root_cmd.clear_children(); root_cmd.clear_children();
@ -124,13 +181,13 @@ fn manage_buttons(
.split("/") .split("/")
.last() .last()
.expect("Extracting filename") .expect("Extracting filename")
.strip_suffix(".otf") .trim_end_matches(".otf")
.expect("Stripping prefix"); .trim_end_matches(".ttf");
let style = TextStyle { let style = TextStyle {
font: font.clone(), font: font.clone(),
color: Color::BLACK, color: Color::BLACK,
font_size: 18.0, font_size: 16.0,
}; };
root.spawn(( root.spawn((
@ -147,33 +204,44 @@ fn manage_buttons(
..default() ..default()
}, },
FontButton(font.clone()), FontButton(font.clone()),
Marker,
)) ))
.with_children(|parent| { .with_children(|parent| {
info!("Adding {} button", fname); info!("Adding {} button", fname);
parent.spawn((TextBundle::from_section(fname, style), Marker)); parent.spawn(TextBundle::from_section(fname, style));
}); });
}); });
}); });
} }
} }
fn update( fn manage_fonts(
mut texts: Query<&mut Text, (With<Marker>, With<PreviewText>)>, mut texts: Query<&mut Text, With<PreviewText>>,
interaction: Query<(&Interaction, &FontButton), Changed<Interaction>>, interaction: Query<(&Interaction, &FontButton), Changed<Interaction>>,
) { ) {
for (i, f) in interaction.iter() { for (i, f) in interaction.iter() {
match (i, f) { match (i, f) {
(Interaction::Clicked, FontButton(font)) => { (Interaction::Pressed, FontButton(font)) => {
let mut text = texts.single_mut(); texts.single_mut().sections.iter_mut().for_each(|section| {
*text = Text::from_sections(LOREM.iter().map(|&s| TextSection { section.style.font = font.clone();
value: s.into(), });
style: TextStyle { }
font: font.clone(), _ => (),
font_size: 50.0, }
..default() }
}, }
}));
fn manage_animation_button(
mut animation_button: Query<&mut Text, With<AnimationButton>>,
interaction: Query<(&Interaction, &FontButton), Changed<Interaction>>,
) {
for (i, f) in interaction.iter() {
match (i, f) {
(Interaction::Pressed, FontButton(font)) => {
animation_button
.single_mut()
.sections
.iter_mut()
.for_each(|section| section.style.font = font.clone());
} }
_ => (), _ => (),
} }
@ -188,8 +256,27 @@ fn mouse_cursor(
let mut window = windows.single_mut(); let mut window = windows.single_mut();
window.cursor.icon = match interaction { window.cursor.icon = match interaction {
Interaction::Hovered | Interaction::Clicked => CursorIcon::Hand, Interaction::Hovered | Interaction::Pressed => CursorIcon::Hand,
Interaction::None => CursorIcon::Arrow, Interaction::None => CursorIcon::Arrow,
} }
} }
} }
fn manage_animation(
mut animated_texts: Query<&mut AnimatedText, With<PreviewText>>,
interactions: Query<&Interaction, (Changed<Interaction>, With<AnimationButton>)>,
) {
for interaction in interactions.iter() {
match interaction {
Interaction::Pressed => {
info!("toggling animation");
let mut anim = animated_texts
.get_single_mut()
.expect("Loading animated text");
anim.toggle();
}
_ => (),
}
}
}

@ -0,0 +1,114 @@
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);
}
}

@ -0,0 +1,3 @@
pub mod debug;
pub mod text;

@ -0,0 +1,177 @@
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()))
}
}
}
},
}
}
}

14
tmp

@ -0,0 +1,14 @@
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…
Cancel
Save