use bevy::input::mouse::{MouseMotion, MouseWheel};
use bevy::prelude::*;
use nalgebra::{DMatrix, DVector};
#[derive(Component)]
struct AnimatedLine2D {
points: DMatrix<f32>,
_times: DVector<f32>,
current_index: usize,
timer: Timer,
}
#[derive(Component)]
pub struct Camera2DController {
pub offset: Vec2,
pub zoom: f32,
}
#[derive(Component)]
struct LegendMarker;
#[derive(Resource)]
struct LineData2D {
points: DMatrix<f32>,
times: DVector<f32>,
axis_names: (String, String),
speed_multiplier: f32,
}
pub fn create_2d_animation(
points: DMatrix<f64>,
times: DVector<f64>,
axes_names: Option<(String, String)>,
speed: Option<f32>,
) {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup_2d_scene)
.add_systems(Update, (animate_2d_line, camera_2d_controller))
.insert_resource(LineData2D {
points: points.cast::<f32>(), times: times.cast::<f32>(),
axis_names: axes_names.unwrap_or_else(|| ("x".to_string(), "y".to_string())),
speed_multiplier: speed.unwrap_or(1.0),
})
.run(); }
fn setup_2d_scene(mut commands: Commands, line_data: Res<LineData2D>) {
commands.insert_resource(ClearColor(Color::srgb(0.95, 0.95, 0.98)));
let p = &line_data.points;
let mut min_vals = [f32::INFINITY; 2];
let mut max_vals = [f32::NEG_INFINITY; 2];
for col in 0..p.ncols() {
for row in 0..2 {
let val = p[(row, col)];
min_vals[row] = min_vals[row].min(val);
max_vals[row] = max_vals[row].max(val);
}
}
let _center = Vec2::new(
(min_vals[0] + max_vals[0]) / 2.0,
(min_vals[1] + max_vals[1]) / 2.0,
);
let size = ((max_vals[0] - min_vals[0]).powi(2) + (max_vals[1] - min_vals[1]).powi(2)).sqrt();
let _initial_zoom = if size > 0.0 {
(2.0 / size).clamp(0.01, 100.0)
} else {
1.0
};
commands.spawn((
Camera2d,
Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)),
Camera2DController {
offset: Vec2::ZERO,
zoom: 1.0,
},
));
let t = &line_data.times;
if p.nrows() != 2 {
error!("Points must have 2 rows (x,y). 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((AnimatedLine2D {
points: p.clone(),
_times: t.clone(),
current_index: 0, timer: Timer::from_seconds(initial_dt, TimerMode::Once), },));
println!(
"2D Axes: {} (red), {} (green)",
line_data.axis_names.0, line_data.axis_names.1
);
println!("Controls: Left-click drag to pan, scroll to zoom");
commands
.spawn((
Text::new("Position: X:0.0, Y: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)",
line_data.axis_names.0, line_data.axis_names.1
)),
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_2d_line(
time: Res<Time>, mut gizmos: Gizmos, mut query: Query<&mut AnimatedLine2D>, line_data: Res<LineData2D>, 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; 2];
let mut max_vals = [f32::NEG_INFINITY; 2];
for col in 0..npoints {
for row in 0..2 {
let val = line.points[(row, col)];
min_vals[row] = min_vals[row].min(val);
max_vals[row] = max_vals[row].max(val);
}
}
let x_extent = (max_vals[0] - min_vals[0]).max(1.0) * 0.6;
let y_extent = (max_vals[1] - min_vals[1]).max(1.0) * 0.6;
gizmos.line_2d(
Vec2::new(-x_extent, 0.0),
Vec2::new(x_extent, 0.0),
Color::srgb(1.0, 0.0, 0.0),
); gizmos.line_2d(
Vec2::new(0.0, -y_extent),
Vec2::new(0.0, y_extent),
Color::srgb(0.0, 1.0, 0.0),
);
let circle_radius = (x_extent + y_extent) * 0.01;
gizmos.circle_2d(
Vec2::new(x_extent, 0.0),
circle_radius,
Color::srgb(1.0, 0.0, 0.0),
);
gizmos.circle_2d(
Vec2::new(-x_extent, 0.0),
circle_radius,
Color::srgb(1.0, 0.0, 0.0),
);
gizmos.circle_2d(
Vec2::new(0.0, y_extent),
circle_radius,
Color::srgb(0.0, 1.0, 0.0),
);
gizmos.circle_2d(
Vec2::new(0.0, -y_extent),
circle_radius,
Color::srgb(0.0, 1.0, 0.0),
);
for i in 0..line.current_index {
if i + 1 < npoints {
let start = Vec2::new(
line.points[(0, i)], line.points[(1, i)], );
let end = Vec2::new(line.points[(0, i + 1)], line.points[(1, i + 1)]);
gizmos.line_2d(start, end, Color::srgb(0.0, 1.0, 1.0));
}
}
let cur_idx = line.current_index.min(npoints - 1); let current_point = Vec2::new(line.points[(0, cur_idx)], line.points[(1, cur_idx)]);
gizmos.circle_2d(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}",
line_data.axis_names.0, current_point.x, line_data.axis_names.1, current_point.y
);
}
}
}
fn camera_2d_controller(
mut mouse_motion: MessageReader<MouseMotion>,
mut scroll_events: MessageReader<MouseWheel>,
mouse_input: Res<ButtonInput<MouseButton>>,
mut camera_query: Query<(&mut Transform, &mut Camera2DController), With<Camera2d>>,
) {
for (mut transform, mut controller) in camera_query.iter_mut() {
if mouse_input.pressed(MouseButton::Left) {
for motion in mouse_motion.read() {
let pan_speed = 2.0;
controller.offset.x -= motion.delta.x * pan_speed / controller.zoom; controller.offset.y += motion.delta.y * pan_speed / controller.zoom;
}
}
for scroll in scroll_events.read() {
controller.zoom *= 1.0 + scroll.y * 0.1;
controller.zoom = controller.zoom.clamp(0.1, 10.0);
}
transform.translation.x = controller.offset.x;
transform.translation.y = controller.offset.y;
transform.scale = Vec3::splat(1.0 / controller.zoom);
}
if !mouse_input.pressed(MouseButton::Left) {
mouse_motion.clear();
}
}
pub fn generate_line_2d(num_points: usize) -> (DMatrix<f64>, DVector<f64>) {
let mut positions = DMatrix::zeros(2, 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; times[i] = t;
}
(positions, times)
}
pub fn generate_circle_2d(num_points: usize, radius: f64) -> (DMatrix<f64>, DVector<f64>) {
let mut positions = DMatrix::zeros(2, 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(); times[i] = t;
}
(positions, times)
}
pub fn generate_sine_wave_2d(
num_points: usize,
amplitude: f64,
frequency: f64,
length: f64,
) -> (DMatrix<f64>, DVector<f64>) {
let mut positions = DMatrix::zeros(2, 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(); times[i] = t;
}
(positions, times)
}
pub fn generate_lissajous_2d(
num_points: usize,
a: f64,
b: f64,
delta: f64,
) -> (DMatrix<f64>, DVector<f64>) {
let mut positions = DMatrix::zeros(2, 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)] = (a * angle).sin(); positions[(1, i)] = (b * angle + delta).sin(); times[i] = t;
}
(positions, times)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_2d_data_generation() {
let (line_pos, line_times) = generate_line_2d(100);
assert_eq!(line_pos.nrows(), 2);
assert_eq!(line_pos.ncols(), 100);
assert_eq!(line_times.len(), 100);
let (circle_pos, _circle_times) = generate_circle_2d(200, 2.0);
assert_eq!(circle_pos.nrows(), 2);
assert_eq!(circle_pos.ncols(), 200);
let (sine_pos, _sine_times) = generate_sine_wave_2d(150, 2.0, 3.0, 10.0);
assert_eq!(sine_pos.nrows(), 2);
assert_eq!(sine_pos.ncols(), 150);
let (liss_pos, _liss_times) =
generate_lissajous_2d(300, 3.0, 2.0, std::f64::consts::PI / 2.0);
assert_eq!(liss_pos.nrows(), 2);
assert_eq!(liss_pos.ncols(), 300);
println!("All 2D shapes generated successfully");
}
}