use bevy::input::mouse::{MouseMotion, MouseWheel};
use bevy::prelude::*;
use nalgebra::{DMatrix, DVector};
use super::animation::normalize_for_display;
use super::animation::{AnimationInputError, advance_animation_clock, validate_trajectory};
#[derive(Component)]
struct AnimatedLine {
points: DMatrix<f32>,
current_index: usize,
elapsed_in_interval: f64,
}
#[derive(Component)]
pub struct CameraController {
pub center: Vec3,
pub distance: f32,
pub yaw: f32,
pub pitch: f32,
}
#[derive(Component)]
struct LegendMarker;
#[derive(Resource)]
struct LineData {
points: DMatrix<f32>,
frame_intervals: Vec<f32>,
min: Vec3,
max: Vec3,
axis_names: (String, String, String),
coordinate_origin: [f64; 3],
units_per_display_unit: f64,
}
pub fn create_3d_animation(
points: DMatrix<f64>,
times: DVector<f64>,
axes_names: Option<(String, String, String)>,
speed: Option<f32>,
) {
if let Err(error) = try_create_3d_animation(points, times, axes_names, speed) {
eprintln!("Cannot start 3D trajectory animation: {error}");
}
}
pub fn try_create_3d_animation(
points: DMatrix<f64>,
times: DVector<f64>,
axes_names: Option<(String, String, String)>,
speed: Option<f32>,
) -> Result<(), AnimationInputError> {
let speed = speed.unwrap_or(1.0);
let frame_intervals = validate_trajectory::<3>(&points, ×, speed)?;
let display = normalize_for_display::<3>(&points);
let points = display.points;
let min = Vec3::new(
(0..points.ncols())
.map(|column| points[(0, column)])
.fold(f32::INFINITY, f32::min),
(0..points.ncols())
.map(|column| points[(1, column)])
.fold(f32::INFINITY, f32::min),
(0..points.ncols())
.map(|column| points[(2, column)])
.fold(f32::INFINITY, f32::min),
);
let max = Vec3::new(
(0..points.ncols())
.map(|column| points[(0, column)])
.fold(f32::NEG_INFINITY, f32::max),
(0..points.ncols())
.map(|column| points[(1, column)])
.fold(f32::NEG_INFINITY, f32::max),
(0..points.ncols())
.map(|column| points[(2, column)])
.fold(f32::NEG_INFINITY, f32::max),
);
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup_scene)
.add_systems(
Update,
(animate_line, camera_controller, exit_animation_on_escape),
)
.insert_resource(LineData {
points,
frame_intervals,
min,
max,
axis_names: axes_names
.unwrap_or_else(|| ("x".to_string(), "y".to_string(), "z".to_string())),
coordinate_origin: display.origin,
units_per_display_unit: display.units_per_display_unit,
})
.run(); Ok(())
}
fn setup_scene(mut commands: Commands, line_data: Res<LineData>) {
commands.insert_resource(ClearColor(Color::srgb(0.95, 0.95, 0.98)));
let center = line_data.min * 0.5 + line_data.max * 0.5;
let extent = line_data.max - line_data.min;
let size = extent.x.max(extent.y).max(extent.z);
let camera_distance = if size > 0.0 { size * 4.0 } else { 2.0 };
commands.spawn((
Camera3d::default(),
Transform::from_xyz(
center.x + camera_distance,
center.y + camera_distance,
center.z + camera_distance,
)
.looking_at(center, Vec3::Y), CameraController {
center,
distance: camera_distance,
yaw: 45.0_f32.to_radians(), pitch: -35.0_f32.to_radians(), },
));
commands.spawn((
DirectionalLight {
illuminance: 20000.0, ..default()
},
Transform::from_rotation(Quat::from_euler(
EulerRot::ZYX,
0.0,
1.0,
-std::f32::consts::FRAC_PI_4,
)),
));
let p = &line_data.points;
commands.spawn((AnimatedLine {
points: p.clone(),
current_index: 0, elapsed_in_interval: 0.0,
},));
println!(
"Axes: {} (red), {} (green), {} (blue)",
line_data.axis_names.0, line_data.axis_names.1, line_data.axis_names.2
);
println!("Controls: Left-click drag to rotate, scroll to zoom");
commands
.spawn((
Text::new("Position: X:0.0, Y:0.0, Z:0.0"),
Node {
position_type: PositionType::Absolute,
bottom: Val::Px(20.0), right: Val::Px(20.0), ..default()
},
TextColor(Color::srgb(0.0, 0.0, 0.0)), TextFont {
font_size: FontSize::Px(16.0),
..default()
},
))
.insert(Name::new("PositionText"));
commands.spawn((
Text::new(format!(
"[R] {} (X-axis)\n[G] {} (Y-axis)\n[B] {} (Z-axis)",
line_data.axis_names.0, line_data.axis_names.1, line_data.axis_names.2
)),
Node {
position_type: PositionType::Absolute,
bottom: Val::Px(60.0), right: Val::Px(20.0),
..default()
},
TextColor(Color::srgb(0.6, 0.6, 0.6)), TextFont {
font_size: FontSize::Px(14.0),
..default()
},
LegendMarker, ));
}
fn animate_line(
time: Res<Time>, mut gizmos: Gizmos, mut query: Query<&mut AnimatedLine>, line_data: Res<LineData>, mut text_query: Query<&mut Text, With<Name>>, ) {
for mut line in query.iter_mut() {
let npoints = line.points.ncols();
if npoints == 0 {
continue; }
(line.current_index, line.elapsed_in_interval) = advance_animation_clock(
line.current_index,
line.elapsed_in_interval,
time.delta().as_secs_f64(),
&line_data.frame_intervals,
);
let ranges = [
line_data.max.x - line_data.min.x,
line_data.max.y - line_data.min.y,
line_data.max.z - line_data.min.z,
];
let max_range = ranges.iter().fold(0.0f32, |a, &b| a.max(b));
let axis_length = if max_range > 0.0 {
max_range * 1.2
} else {
1.2
};
let axis_origin = line_data.min * 0.5 + line_data.max * 0.5;
let x_end = axis_origin + Vec3::X * axis_length; let y_end = axis_origin + Vec3::Y * axis_length; let z_end = axis_origin + Vec3::Z * axis_length;
gizmos.line(axis_origin, x_end, Color::srgb(1.0, 0.0, 0.0)); gizmos.line(axis_origin, y_end, Color::srgb(0.0, 1.0, 0.0)); gizmos.line(axis_origin, z_end, Color::srgb(0.0, 0.0, 1.0));
let sphere_radius = axis_length * 0.02;
gizmos.sphere(x_end, sphere_radius, Color::srgb(1.0, 0.0, 0.0));
gizmos.sphere(y_end, sphere_radius, Color::srgb(0.0, 1.0, 0.0));
gizmos.sphere(z_end, sphere_radius, Color::srgb(0.0, 0.0, 1.0));
for i in 0..line.current_index {
if i + 1 < npoints {
let start = Vec3::new(
line.points[(0, i)], line.points[(1, i)], line.points[(2, i)], );
let end = Vec3::new(
line.points[(0, i + 1)],
line.points[(1, i + 1)],
line.points[(2, i + 1)],
);
gizmos.line(start, end, Color::srgb(0.0, 1.0, 1.0));
}
}
let cur_idx = line.current_index.min(npoints - 1); let current_point = Vec3::new(
line.points[(0, cur_idx)],
line.points[(1, cur_idx)],
line.points[(2, cur_idx)],
);
gizmos.sphere(
current_point,
(axis_length * 0.01).max(1.0e-6),
Color::srgb(0.0, 0.0, 1.0),
);
for mut text in text_query.iter_mut() {
let physical_x = f64::from(current_point.x) * line_data.units_per_display_unit
+ line_data.coordinate_origin[0];
let physical_y = f64::from(current_point.y) * line_data.units_per_display_unit
+ line_data.coordinate_origin[1];
let physical_z = f64::from(current_point.z) * line_data.units_per_display_unit
+ line_data.coordinate_origin[2];
**text = format!(
"Position: {}:{:.2}, {}:{:.2}, {}:{:.2}",
line_data.axis_names.0,
physical_x,
line_data.axis_names.1,
physical_y,
line_data.axis_names.2,
physical_z
);
}
}
}
fn camera_controller(
mut mouse_motion: MessageReader<MouseMotion>, mut scroll_events: MessageReader<MouseWheel>, mouse_input: Res<ButtonInput<MouseButton>>, mut camera_query: Query<(&mut Transform, &mut CameraController), With<Camera3d>>, ) {
for (mut transform, mut controller) in camera_query.iter_mut() {
if mouse_input.pressed(MouseButton::Left) {
for motion in mouse_motion.read() {
controller.yaw -= motion.delta.x * 0.01; controller.pitch -= motion.delta.y * 0.01; controller.pitch = controller.pitch.clamp(-1.5, 1.5);
}
}
for scroll in scroll_events.read() {
controller.distance *= 1.0 - scroll.y * 0.1;
controller.distance = controller.distance.clamp(1.0e-6, 1.0e9);
}
let x = controller.center.x
+ controller.distance * controller.yaw.cos() * controller.pitch.cos();
let y = controller.center.y + controller.distance * controller.pitch.sin();
let z = controller.center.z
+ controller.distance * controller.yaw.sin() * controller.pitch.cos();
transform.translation = Vec3::new(x, y, z);
transform.look_at(controller.center, Vec3::Y); }
if !mouse_input.pressed(MouseButton::Left) {
mouse_motion.clear();
}
}
fn exit_animation_on_escape(
keyboard: Res<ButtonInput<KeyCode>>,
mut app_exit: MessageWriter<AppExit>,
) {
if keyboard.just_pressed(KeyCode::Escape) {
app_exit.write(AppExit::Success);
}
}
pub fn generate_line(num_points: usize) -> (DMatrix<f64>, DVector<f64>) {
let mut positions = DMatrix::zeros(3, num_points);
let mut times = DVector::zeros(num_points);
for i in 0..num_points {
let t = i as f64 / (num_points - 1) as f64; positions[(0, i)] = t * 5.0; positions[(1, i)] = 0.0; positions[(2, i)] = 0.0; times[i] = t;
}
(positions, times)
}
pub fn generate_circle(num_points: usize, radius: f64) -> (DMatrix<f64>, DVector<f64>) {
let mut positions = DMatrix::zeros(3, num_points);
let mut times = DVector::zeros(num_points);
for i in 0..num_points {
let t = i as f64 / (num_points - 1) as f64;
let angle = t * 2.0 * std::f64::consts::PI; positions[(0, i)] = radius * angle.cos(); positions[(1, i)] = radius * angle.sin(); positions[(2, i)] = 0.0; times[i] = t;
}
(positions, times)
}
pub fn generate_helix(
num_points: usize,
radius: f64,
height: f64,
turns: f64,
) -> (DMatrix<f64>, DVector<f64>) {
let mut positions = DMatrix::zeros(3, num_points);
let mut times = DVector::zeros(num_points);
for i in 0..num_points {
let t = i as f64 / (num_points - 1) as f64;
let angle = t * turns * 2.0 * std::f64::consts::PI; positions[(0, i)] = radius * angle.cos(); positions[(1, i)] = radius * angle.sin(); positions[(2, i)] = t * height; times[i] = t;
}
(positions, times)
}
pub fn generate_sine_wave(
num_points: usize,
amplitude: f64,
frequency: f64,
length: f64,
) -> (DMatrix<f64>, DVector<f64>) {
let mut positions = DMatrix::zeros(3, num_points);
let mut times = DVector::zeros(num_points);
for i in 0..num_points {
let t = i as f64 / (num_points - 1) as f64;
let x = t * length; positions[(0, i)] = x;
positions[(1, i)] = amplitude * (frequency * x).sin(); positions[(2, i)] = 0.0; times[i] = t;
}
(positions, times)
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_lorenz_data(num_points: usize) -> (DMatrix<f32>, DVector<f32>) {
let mut positions = DMatrix::zeros(3, num_points);
let mut time = DVector::zeros(num_points);
let mut x = 1.0;
let mut y = 1.0;
let mut z = 1.0;
let sigma = 10.0;
let rho = 28.0;
let beta = 8.0 / 3.0;
let dt = 0.01;
for i in 0..num_points {
let dx = sigma * (y - x);
let dy = x * (rho - z) - y;
let dz = x * y - beta * z;
x += dx * dt;
y += dy * dt;
z += dz * dt;
positions[(0, i)] = x as f32;
positions[(1, i)] = y as f32;
positions[(2, i)] = z as f32;
time[i] = i as f32 * dt as f32;
}
(positions, time)
}
#[test]
fn test_lorenz_data_generation() {
let (positions, times) = generate_lorenz_data(1000);
let positions_f64 = positions.cast::<f64>();
let times_f64 = times.cast::<f64>();
assert_eq!(positions_f64.nrows(), 3);
assert_eq!(positions_f64.ncols(), 1000);
assert_eq!(times_f64.len(), 1000);
assert!(times_f64[1] > times_f64[0]);
assert!(times_f64[999] > times_f64[0]);
println!("Lorenz data generated successfully for animation");
println!(
"First point: ({}, {}, {})",
positions_f64[(0, 0)],
positions_f64[(1, 0)],
positions_f64[(2, 0)]
);
println!(
"Last point: ({}, {}, {})",
positions_f64[(0, 999)],
positions_f64[(1, 999)],
positions_f64[(2, 999)]
);
}
#[test]
fn test_geometric_shapes() {
let (line_pos, line_times) = generate_line(100);
assert_eq!(line_pos.nrows(), 3);
assert_eq!(line_pos.ncols(), 100);
assert_eq!(line_times.len(), 100);
let (circle_pos, _circle_times) = generate_circle(200, 2.0);
assert_eq!(circle_pos.nrows(), 3);
assert_eq!(circle_pos.ncols(), 200);
let (helix_pos, _helix_times) = generate_helix(300, 1.5, 5.0, 3.0);
assert_eq!(helix_pos.nrows(), 3);
assert_eq!(helix_pos.ncols(), 300);
let (sine_pos, _sine_times) = generate_sine_wave(150, 2.0, 3.0, 10.0);
assert_eq!(sine_pos.nrows(), 3);
assert_eq!(sine_pos.ncols(), 150);
println!("All geometric shapes generated successfully");
}
}