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 AnimatedLine2D {
points: DMatrix<f32>,
current_index: usize,
elapsed_in_interval: f64,
}
#[derive(Component)]
pub struct Camera2DController {
pub offset: Vec2,
pub zoom: f32,
}
#[derive(Component)]
struct LegendMarker;
#[derive(Resource)]
struct LineData2D {
points: DMatrix<f32>,
frame_intervals: Vec<f32>,
min: Vec2,
max: Vec2,
axis_names: (String, String),
coordinate_origin: [f64; 2],
units_per_display_unit: f64,
}
pub fn create_2d_animation(
points: DMatrix<f64>,
times: DVector<f64>,
axes_names: Option<(String, String)>,
speed: Option<f32>,
) {
if let Err(error) = try_create_2d_animation(points, times, axes_names, speed) {
eprintln!("Cannot start 2D trajectory animation: {error}");
}
}
pub fn try_create_2d_animation(
points: DMatrix<f64>,
times: DVector<f64>,
axes_names: Option<(String, String)>,
speed: Option<f32>,
) -> Result<(), AnimationInputError> {
let speed = speed.unwrap_or(1.0);
let frame_intervals = validate_trajectory::<2>(&points, ×, speed)?;
let display = normalize_for_display::<2>(&points);
let points = display.points;
let min = Vec2::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),
);
let max = Vec2::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),
);
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup_2d_scene)
.add_systems(
Update,
(
animate_2d_line,
camera_2d_controller,
exit_animation_on_escape,
),
)
.insert_resource(LineData2D {
points,
frame_intervals,
min,
max,
axis_names: axes_names.unwrap_or_else(|| ("x".to_string(), "y".to_string())),
coordinate_origin: display.origin,
units_per_display_unit: display.units_per_display_unit,
})
.run(); Ok(())
}
fn setup_2d_scene(mut commands: Commands, line_data: Res<LineData2D>, windows: Query<&Window>) {
commands.insert_resource(ClearColor(Color::srgb(0.95, 0.95, 0.98)));
let p = &line_data.points;
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);
let viewport_height = windows.iter().next().map(Window::height).unwrap_or(720.0);
let initial_camera_scale = if size > 0.0 {
(size / (viewport_height * 0.7)).clamp(1.0e-6, 1.0e6)
} else {
1.0
};
commands.spawn((
Camera2d,
Transform::from_translation(Vec3::new(center.x, center.y, 0.0))
.with_scale(Vec3::splat(initial_camera_scale)),
Camera2DController {
offset: center,
zoom: 1.0 / initial_camera_scale,
},
));
commands.spawn((AnimatedLine2D {
points: p.clone(),
current_index: 0, elapsed_in_interval: 0.0,
},));
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.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 center = line_data.min * 0.5 + line_data.max * 0.5;
let data_extent = line_data.max - line_data.min;
let overall_extent = data_extent.x.max(data_extent.y);
let fallback_extent = if overall_extent > 0.0 {
overall_extent * 0.1
} else {
1.0
};
let x_extent = data_extent.x.max(fallback_extent).max(1.0e-6) * 0.6;
let y_extent = data_extent.y.max(fallback_extent).max(1.0e-6) * 0.6;
gizmos.line_2d(
Vec2::new(center.x - x_extent, center.y),
Vec2::new(center.x + x_extent, center.y),
Color::srgb(1.0, 0.0, 0.0),
); gizmos.line_2d(
Vec2::new(center.x, center.y - y_extent),
Vec2::new(center.x, center.y + y_extent),
Color::srgb(0.0, 1.0, 0.0),
);
let circle_radius = (x_extent + y_extent) * 0.01;
gizmos.circle_2d(
Vec2::new(center.x + x_extent, center.y),
circle_radius,
Color::srgb(1.0, 0.0, 0.0),
);
gizmos.circle_2d(
Vec2::new(center.x - x_extent, center.y),
circle_radius,
Color::srgb(1.0, 0.0, 0.0),
);
gizmos.circle_2d(
Vec2::new(center.x, center.y + y_extent),
circle_radius,
Color::srgb(0.0, 1.0, 0.0),
);
gizmos.circle_2d(
Vec2::new(center.x, center.y - 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() {
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];
**text = format!(
"Position: {}:{:.2}, {}:{:.2}",
line_data.axis_names.0, physical_x, line_data.axis_names.1, physical_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(1.0e-6, 1.0e6);
}
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();
}
}
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_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");
}
}