use bevy::color::palettes::basic::{GRAY, RED, WHITE};
use bevy::input::mouse::{MouseButtonInput, MouseMotion};
use bevy::prelude::*;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (draw_cube_axes, draw_random_axes))
.add_systems(Update, (handle_keypress, handle_mouse, rotate_cube).chain())
.run();
}
#[derive(Component, Default)]
struct Cube {
initial_transform: Transform,
target_transform: Transform,
progress: u16,
in_motion: bool,
}
#[derive(Component)]
struct RandomAxes(Dir3, Dir3);
#[derive(Component)]
struct Instructions;
#[derive(Resource)]
struct MousePressed(bool);
#[derive(Resource)]
struct SeededRng(ChaCha8Rng);
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let mut seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(3., 2.5, 4.).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
commands.spawn(PbrBundle {
transform: Transform::from_xyz(0., -2., 0.),
mesh: meshes.add(Plane3d::default().mesh().size(100.0, 100.0)),
material: materials.add(Color::srgb(0.3, 0.5, 0.3)),
..default()
});
commands.spawn(PointLightBundle {
point_light: PointLight {
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 7.0, -4.0),
..default()
});
let first = seeded_rng.gen();
let second = seeded_rng.gen();
commands.spawn(RandomAxes(first, second));
commands.spawn((
PbrBundle {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::srgb(0.5, 0.5, 0.5)),
..default()
},
Cube {
initial_transform: Transform::IDENTITY,
target_transform: random_axes_target_alignment(&RandomAxes(first, second)),
..default()
},
));
commands.spawn((
TextBundle::from_section(
"The bright red axis is the primary alignment axis, and it will always be\n\
made to coincide with the primary target direction (white) exactly.\n\
The fainter red axis is the secondary alignment axis, and it is made to\n\
line up with the secondary target direction (gray) as closely as possible.\n\
Press 'R' to generate random target directions.\n\
Press 'T' to align the cube to those directions.\n\
Click and drag the mouse to rotate the camera.\n\
Press 'H' to hide/show these instructions.",
TextStyle {
font_size: 20.,
..default()
},
)
.with_style(Style {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
}),
Instructions,
));
commands.insert_resource(MousePressed(false));
commands.insert_resource(SeededRng(seeded_rng));
}
fn draw_cube_axes(mut gizmos: Gizmos, query: Query<&Transform, With<Cube>>) {
let cube_transform = query.single();
let x_ends = arrow_ends(cube_transform, Vec3::X, 1.5);
gizmos.arrow(x_ends.0, x_ends.1, RED);
let y_ends = arrow_ends(cube_transform, Vec3::Y, 1.5);
gizmos.arrow(y_ends.0, y_ends.1, Color::srgb(0.65, 0., 0.));
}
fn draw_random_axes(mut gizmos: Gizmos, query: Query<&RandomAxes>) {
let RandomAxes(v1, v2) = query.single();
gizmos.arrow(Vec3::ZERO, 1.5 * *v1, WHITE);
gizmos.arrow(Vec3::ZERO, 1.5 * *v2, GRAY);
}
fn rotate_cube(mut cube: Query<(&mut Cube, &mut Transform)>) {
let (mut cube, mut cube_transform) = cube.single_mut();
if !cube.in_motion {
return;
}
let start = cube.initial_transform.rotation;
let end = cube.target_transform.rotation;
let p: f32 = cube.progress.into();
let t = p / 100.;
*cube_transform = Transform::from_rotation(start.slerp(end, t));
if cube.progress == 100 {
cube.in_motion = false;
} else {
cube.progress += 1;
}
}
fn handle_keypress(
mut cube: Query<(&mut Cube, &Transform)>,
mut random_axes: Query<&mut RandomAxes>,
mut instructions: Query<&mut Visibility, With<Instructions>>,
keyboard: Res<ButtonInput<KeyCode>>,
mut seeded_rng: ResMut<SeededRng>,
) {
let (mut cube, cube_transform) = cube.single_mut();
let mut random_axes = random_axes.single_mut();
if keyboard.just_pressed(KeyCode::KeyR) {
let first = seeded_rng.0.gen();
let second = seeded_rng.0.gen();
*random_axes = RandomAxes(first, second);
cube.in_motion = false;
cube.initial_transform = *cube_transform;
cube.target_transform = random_axes_target_alignment(&random_axes);
cube.progress = 0;
}
if keyboard.just_pressed(KeyCode::KeyT) {
cube.in_motion ^= true;
}
if keyboard.just_pressed(KeyCode::KeyH) {
let mut instructions_viz = instructions.single_mut();
if *instructions_viz == Visibility::Hidden {
*instructions_viz = Visibility::Visible;
} else {
*instructions_viz = Visibility::Hidden;
}
}
}
fn handle_mouse(
mut button_events: EventReader<MouseButtonInput>,
mut motion_events: EventReader<MouseMotion>,
mut camera: Query<&mut Transform, With<Camera>>,
mut mouse_pressed: ResMut<MousePressed>,
) {
for button_event in button_events.read() {
if button_event.button != MouseButton::Left {
continue;
}
*mouse_pressed = MousePressed(button_event.state.is_pressed());
}
if !mouse_pressed.0 {
return;
}
let displacement = motion_events
.read()
.fold(0., |acc, mouse_motion| acc + mouse_motion.delta.x);
let mut camera_transform = camera.single_mut();
camera_transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(-displacement / 75.));
}
fn arrow_ends(transform: &Transform, axis: Vec3, length: f32) -> (Vec3, Vec3) {
let local_vector = length * (transform.rotation * axis);
(transform.translation, transform.translation + local_vector)
}
fn random_axes_target_alignment(random_axes: &RandomAxes) -> Transform {
let RandomAxes(first, second) = random_axes;
Transform::IDENTITY.aligned_by(Vec3::X, *first, Vec3::Y, *second)
}