You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

499 lines
15 KiB
Rust

use bevy::{gltf::Gltf, prelude::*, utils::HashMap};
use monologue_trees::debug::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "GLTF Inspector".into(),
resolution: (640., 480.).into(),
..default()
}),
..default()
}),
DebugInfoPlugin,
))
.add_event::<ResetScene>()
.add_event::<SpawnScene>()
.init_resource::<Current>()
.add_systems(Startup, spawn_ui)
.add_systems(PreUpdate, (add_camera_ui, add_scene_ui, add_animation_ui))
.add_systems(
Update,
(
drag_and_drop,
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();
}
#[derive(Component)]
struct SceneMarker;
#[derive(Component)]
struct DefaultCamera;
#[derive(Component)]
struct DefaultLight;
#[derive(Component)]
struct MainUi;
#[derive(Component)]
struct UiTitle;
#[derive(Component)]
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)]
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)]
struct ResetAnimation;
fn drag_and_drop(
mut events: EventReader<FileDragAndDrop>,
server: Res<AssetServer>,
mut current: ResMut<Current>,
) {
events
.iter()
.filter_map(|event| match event {
FileDragAndDrop::DroppedFile { path_buf, .. } => Some(path_buf),
_ => None,
})
.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)
// TODO: Scene select container
// TODO: Animation Play/Pause Placeholder
commands.spawn((
Camera3dBundle {
transform: Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
..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()
},
DefaultLight,
));
commands
.spawn((
NodeBundle {
style: Style {
flex_wrap: FlexWrap::Wrap,
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceAround,
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
..default()
},
MainUi,
))
.with_children(|parent| {
parent
.spawn(NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
..default()
},
..default()
})
.with_children(|parent| {
parent.spawn((
TextBundle::from_section(
"Drag and Drop .gltf/.glb file",
TextStyle {
color: Color::WHITE,
..default()
},
),
InstructionsUi,
));
parent.spawn((
TextBundle::from_section(
"Using default camera",
TextStyle {
color: Color::WHITE,
..default()
},
),
DefaultCamera,
));
parent.spawn((
TextBundle::from_section(
"Using default light",
TextStyle {
color: Color::WHITE,
..default()
},
),
DefaultLight,
));
});
parent
.spawn((
NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
..default()
},
..default()
},
SceneSelectUi,
))
.with_children(|parent| {
parent.spawn((
TextBundle::from_section("Scenes", TextStyle { ..default() }),
UiTitle,
));
});
parent
.spawn((
NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
..default()
},
..default()
},
CameraSelectUi,
))
.with_children(|parent| {
parent.spawn((
TextBundle::from_section("Cameras", TextStyle { ..default() }),
UiTitle,
));
});
parent
.spawn((
NodeBundle {
style: Style {
flex_direction: FlexDirection::Column,
..default()
},
..default()
},
AnimationSelectUi,
))
.with_children(|parent| {
parent.spawn((
TextBundle::from_section("Animations", TextStyle { ..default() }),
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
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())),
_ => (),
});
}
/// 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,
) {
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()
},
SceneButton(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()
},
AnimationButton(handle.clone()),
))
.with_children(|parent| {
parent.spawn(TextBundle::from_section(name, TextStyle { ..default() }));
});
});
});
});
}
fn control_camera(
interactions: Query<(&Interaction, &CameraButton), (Changed<Interaction>, With<Button>)>,
mut cameras: Query<(Entity, &mut Camera), Without<DefaultCamera>>,
) {
interactions
.iter()
.for_each(|(interaction, CameraButton(entity))| match interaction {
Interaction::Pressed => cameras
.iter_mut()
.for_each(|(e, mut camera)| camera.is_active = e == *entity),
_ => (),
})
}
/// 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 there are no other lights, use default light
fn control_default_light(
mut toggle: Query<&mut Visibility, With<DefaultLight>>,
other_lights: Query<
Entity,
(
Or<(With<SpotLight>, With<DirectionalLight>, With<PointLight>)>,
Without<DefaultLight>,
),
>,
) {
toggle.iter_mut().for_each(|mut vis| {
*vis = if other_lights.is_empty() {
Visibility::Visible
} else {
Visibility::Hidden
}
});
}
/// Add camera buttons when a new camera is added/gltf is loaded
fn add_camera_ui(
events: Query<(Entity, &Name), Added<Camera>>,
ui_root: Query<Entity, With<CameraSelectUi>>,
mut commands: Commands,
) {
events.iter().for_each(|(entity, name)| {
commands.entity(ui_root.single()).with_children(|parent| {
parent
.spawn((
ButtonBundle {
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 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
fn reset_scene(
mut events: EventReader<ResetScene>,
mut commands: Commands,
current: Query<Entity, With<SceneMarker>>,
) {
events.iter().for_each(|_| {
info!("Reset scene");
current
.iter()
.for_each(|e| commands.entity(e).despawn_recursive())
});
}
/// Spawn a desired scene
fn spawn_scene(
mut events: EventReader<SpawnScene>,
mut commands: Commands,
current: Query<Entity, With<SceneMarker>>,
) {
// Handle SpawnScene events
events.iter().for_each(|SpawnScene(handle)| {
info!("Reset scene (inline)");
current
.iter()
.for_each(|e| commands.entity(e).despawn_recursive());
info!("Spawning scene {:?}", handle);
commands.spawn((
SceneBundle {
scene: handle.clone(),
..default()
},
SceneMarker,
));
});
}