use bevy::input::mouse::{MouseMotion, MouseWheel};
use bevy::prelude::*;
use nalgebra::{DMatrix, DVector};
#[derive(Component)]
struct AnimatedLine {
points: DMatrix<f32>,
_times: DVector<f32>,
current_index: usize,
timer: Timer,
}
#[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>,
times: DVector<f32>,
axis_names: (String, String, String),
speed_multiplier: f32,
}
pub fn create_3d_animation(
points: DMatrix<f64>,
times: DVector<f64>,
axes_names: Option<(String, String, String)>,
speed: Option<f32>,
) {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup_scene)
.add_systems(Update, (animate_line, camera_controller))
.insert_resource(LineData {
points: points.cast::<f32>(), times: times.cast::<f32>(),
axis_names: axes_names
.unwrap_or_else(|| ("x".to_string(), "y".to_string(), "z".to_string())),
speed_multiplier: speed.unwrap_or(1.0),
})
.run(); }
fn setup_scene(mut commands: Commands, line_data: Res<LineData>) {
commands.insert_resource(ClearColor(Color::srgb(0.95, 0.95, 0.98)));
let p = &line_data.points;
let mut min_vals = [f32::INFINITY; 3];
let mut max_vals = [f32::NEG_INFINITY; 3];
for col in 0..p.ncols() {
for row in 0..3 {
let val = p[(row, col)];
min_vals[row] = min_vals[row].min(val);
max_vals[row] = max_vals[row].max(val);
}
}
let center = Vec3::new(
(min_vals[0] + max_vals[0]) / 2.0,
(min_vals[1] + max_vals[1]) / 2.0,
(min_vals[2] + max_vals[2]) / 2.0,
);
let size = ((max_vals[0] - min_vals[0]).powi(2)
+ (max_vals[1] - max_vals[1]).powi(2)
+ (max_vals[2] - min_vals[2]).powi(2))
.sqrt();
let camera_distance = size * 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;
let t = &line_data.times;
if p.nrows() != 3 {
error!("Points must have 3 rows (x,y,z). got nrows = {}", p.nrows());
return;
}
if p.ncols() == 0 || t.len() == 0 {
warn!("No points/times provided.");
return;
}
if p.ncols() != t.len() {
warn!(
"Number of points (columns) != number of times. points.cols={} times.len={}",
p.ncols(),
t.len()
);
}
let _npoints = p.ncols();
let initial_dt = if t.len() >= 2 {
((t[1] - t[0]).abs().max(1e-6)) / line_data.speed_multiplier
} else {
0.1_f32 / line_data.speed_multiplier
};
commands.spawn((AnimatedLine {
points: p.clone(),
_times: t.clone(),
current_index: 0, timer: Timer::from_seconds(initial_dt, TimerMode::Once), },));
let mut min_vals = [f32::INFINITY; 3];
let mut max_vals = [f32::NEG_INFINITY; 3];
for col in 0..p.ncols() {
for row in 0..3 {
let val = p[(row, col)];
min_vals[row] = min_vals[row].min(val);
max_vals[row] = max_vals[row].max(val);
}
}
let ranges = [
max_vals[0] - min_vals[0],
max_vals[1] - min_vals[1],
max_vals[2] - min_vals[2],
];
let max_range = ranges.iter().fold(0.0f32, |a, &b| a.max(b));
let _axis_length = max_range * 1.2;
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.timer.tick(time.delta());
if line.timer.just_finished() {
let skip = (line_data.speed_multiplier as usize).max(1);
line.current_index = (line.current_index + skip) % npoints;
let base_duration = 0.05;
line.timer = Timer::from_seconds(base_duration, TimerMode::Once);
line.timer.reset();
}
let mut min_vals = [f32::INFINITY; 3];
let mut max_vals = [f32::NEG_INFINITY; 3];
for col in 0..npoints {
for row in 0..3 {
let val = line.points[(row, col)];
min_vals[row] = min_vals[row].min(val);
max_vals[row] = max_vals[row].max(val);
}
}
let ranges = [
max_vals[0] - min_vals[0],
max_vals[1] - min_vals[1],
max_vals[2] - min_vals[2],
];
let max_range = ranges.iter().fold(0.0f32, |a, &b| a.max(b));
let axis_length = max_range * 1.2;
let x_end = Vec3::X * axis_length; let y_end = Vec3::Y * axis_length; let z_end = Vec3::Z * axis_length;
gizmos.line(Vec3::ZERO, x_end, Color::srgb(1.0, 0.0, 0.0)); gizmos.line(Vec3::ZERO, y_end, Color::srgb(0.0, 1.0, 0.0)); gizmos.line(Vec3::ZERO, 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, 0.05, Color::srgb(0.0, 0.0, 1.0));
for mut text in text_query.iter_mut() {
**text = format!(
"Position: {}:{:.2}, {}:{:.2}, {}:{:.2}",
line_data.axis_names.0,
current_point.x,
line_data.axis_names.1,
current_point.y,
line_data.axis_names.2,
current_point.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(0.1, 1000.0);
}
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();
}
}
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");
}
}