use std::collections::BTreeMap; use bevy::prelude::*; use bevy_egui::{egui, EguiContexts, EguiPlugin, EguiPrimaryContextPass}; use bevy_rapier2d::plugin::{NoUserData, RapierPhysicsPlugin}; use ordered_float::OrderedFloat; use ron::ser::PrettyConfig; use crate::{particle_editor::{hooks::hooks_plugin, spawn::spawn_plugin}, particles::{LifetimeCurve, ParticleEffect, RandF32, RandUsize, RandVec2}}; mod spawn; mod hooks; mod ecs; pub fn particle_editor_plugin(app: &mut App) { app.add_plugins(DefaultPlugins); app.add_plugins(EguiPlugin::default()); app.add_plugins(RapierPhysicsPlugin::::pixels_per_meter(10.0)); app.add_systems(Startup, setup_camera_system); app.add_systems(EguiPrimaryContextPass, editor_ui); app.add_systems(Startup, setup_editor_effect); app.add_plugins(spawn_plugin); app.add_plugins(hooks_plugin); app.insert_resource(EditorResource { ser_field: String::new(), status: "Ready".to_string(), add_scale_t: 0.0, add_scale_v: 0.0, add_color_t: 0.0, add_color_v: [0u8; 4], scale_curve: LifetimeCurve::new(&[ (0.0f32, 5.0), (2.0, 0.0) ]).0.iter().map(|u| (u.0.clone(), u.1.clone())).collect::>(), color_curve: vec![(OrderedFloat(0.0f32), [255, 0, 0, 255])] }); } fn setup_editor_effect(mut commands: Commands) { commands.spawn(( ParticleEffect { lifetime_seconds: RandF32 { value: 2.0, randomness: 0.1, }, batch_spawn_delay_seconds: RandF32 { value: 0.1, randomness: 0.05 }, particles_in_batch: RandUsize { value: 1, randomness: 1 }, initial_linear_velocity: RandVec2 { x: RandF32 { value: 0.0, randomness: 0.5 }, y: RandF32 { value: 10.0, randomness: 1.0 } }, initial_angular_velocity: RandF32 { value: 1.0, randomness: 0.5 }, scale: LifetimeCurve::new(&[ (0.0f32, 5.0), (2.0, 0.0) ]), color: LifetimeCurve::new(&[ (0.0f32, Srgba::new(1.0, 0.0, 0.0, 1.0).into()), ]), }, Transform::from_xyz(0.0, 0.0, 0.0), )); } fn setup_camera_system(mut commands: Commands) { commands.spawn((Camera2d, Transform::from_scale(Vec3::splat(0.1)))); } #[derive(Resource)] struct EditorResource { ser_field: String, status: String, add_scale_t: f32, add_scale_v: f32, add_color_t: f32, add_color_v: [u8; 4], scale_curve: Vec<(OrderedFloat, f32)>, color_curve: Vec<(OrderedFloat, [u8; 4])> } fn editor_ui(mut contexts: EguiContexts, effect: Single<&mut ParticleEffect>, mut editor_resource: ResMut) -> Result { let mut effect = effect.into_inner(); egui::Window::new("Particle Effect").resizable(false).show(contexts.ctx_mut()?, |ui| { egui::Grid::new("effect").striped(true).show(ui, |ui| { draw_rand_f32(&mut effect.lifetime_seconds, "Lifetime (seconds): ", ui); draw_rand_f32(&mut effect.batch_spawn_delay_seconds, "Delay in between batches (seconds): ", ui); draw_rand_usize(&mut effect.particles_in_batch, "Number of particles in batch: ", ui); draw_rand_f32(&mut effect.initial_linear_velocity.x, "Linear velocity (x-axis, m/s): ", ui); draw_rand_f32(&mut effect.initial_linear_velocity.y, "Linear velocity (y-axis, m/s): ", ui); draw_rand_f32(&mut effect.initial_angular_velocity, "Angular velocity (radians/second): ", ui); ui.separator(); ui.label("Scale curve"); if ui.button("sort").clicked() { editor_resource.scale_curve.sort_by_key(|u|u.0); } ui.end_row(); editor_resource.scale_curve.retain_mut(|(k, v)| { ui.label("scale t="); ui.add(egui::DragValue::new(k.as_mut()).speed(0.01).range(0.0f32..=effect.lifetime_seconds.value)); ui.label("v="); ui.add(egui::DragValue::new(v).speed(0.01)); let r = ui.button("-"); ui.end_row(); !r.clicked() }); ui.separator(); ui.end_row(); ui.label("new scale: t="); ui.add(egui::DragValue::new(&mut editor_resource.add_scale_t).speed(0.01).range(0.0f32..=effect.lifetime_seconds.value)); ui.label("v="); ui.add(egui::DragValue::new(&mut editor_resource.add_scale_v).speed(0.01)); if ui.button("+").clicked() { let new_v = (OrderedFloat(editor_resource.add_scale_t), editor_resource.add_scale_v).clone(); editor_resource.scale_curve.push(new_v); } ui.end_row(); effect.scale = LifetimeCurve(BTreeMap::from_iter(editor_resource.scale_curve.iter().copied())); ui.separator(); ui.end_row(); ui.separator(); ui.label("Color curve"); if ui.button("sort").clicked() { editor_resource.color_curve.sort_by_key(|u|u.0); } ui.end_row(); editor_resource.color_curve.retain_mut(|(k, v)| { ui.label("color t="); ui.add(egui::DragValue::new(k.as_mut()).speed(0.01).range(0.0f32..=effect.lifetime_seconds.value)); ui.label("v="); ui.color_edit_button_srgba_unmultiplied(v); let r = ui.button("-"); ui.end_row(); !r.clicked() }); ui.separator(); ui.end_row(); ui.label("new color: t="); ui.add(egui::DragValue::new(&mut editor_resource.add_color_t).speed(0.01).range(0.0f32..=effect.lifetime_seconds.value)); ui.label("v="); ui.color_edit_button_srgba_unmultiplied(&mut editor_resource.add_color_v); if ui.button("+").clicked() { let new_v = (OrderedFloat(editor_resource.add_color_t), editor_resource.add_color_v).clone(); editor_resource.color_curve.push(new_v); } ui.end_row(); let curve_copied = editor_resource.color_curve.clone(); effect.color = LifetimeCurve(BTreeMap::from_iter(curve_copied.iter().map( |(u, v)| ( *u, Color::Srgba(Srgba::new( v[0] as f32 / 256.0, v[1] as f32 / 256.0, v[2] as f32 / 256.0, v[3] as f32 / 256.0 )) ) ))); ui.separator(); ui.end_row(); }); ui.horizontal(|ui| { if ui.button("Generate").clicked() { effect.scale = LifetimeCurve(BTreeMap::from_iter(editor_resource.scale_curve.iter().copied())); let curve_copied = editor_resource.color_curve.clone(); effect.color = LifetimeCurve(BTreeMap::from_iter(curve_copied.iter().map( |(u, v)| ( *u, Color::Srgba(Srgba::new( v[0] as f32 / 256.0, v[1] as f32 / 256.0, v[2] as f32 / 256.0, v[3] as f32 / 256.0 )) ) ))); editor_resource.ser_field = ron::ser::to_string(effect.as_ref()).unwrap(); editor_resource.status = "Ready; Generated OK".to_string(); } if ui.button("Load").clicked() { match ron::from_str(&editor_resource.ser_field) { Ok(e) => { *effect = e; editor_resource.scale_curve = effect.scale.0.iter().map(|u| (u.0.clone(), u.1.clone())).collect::>(); editor_resource.color_curve = effect.color.0.iter().map(|u| ( u.0.clone(), { let mut r = [0u8; 4]; let srgba: Srgba = (*u.1).into(); r[0] = (srgba.red * 256.0).floor() as u8; r[1] = (srgba.green * 256.0).floor() as u8; r[2] = (srgba.blue * 256.0).floor() as u8; r[3] = (srgba.alpha * 256.0).floor() as u8; r } )).collect::>(); }, Err(e) => { editor_resource.status = e.to_string(); } }; } }); ui.text_edit_multiline(&mut editor_resource.ser_field); ui.text_edit_multiline(&mut editor_resource.status.as_str()); }); Ok(()) } fn draw_rand_f32(v: &mut RandF32, l: &str, ui: &mut egui::Ui) { ui.label(l); ui.add(egui::DragValue::new(&mut v.value).speed(0.01)); ui.label("variance:"); ui.add(egui::DragValue::new(&mut v.randomness).speed(0.01)); ui.end_row(); } fn draw_rand_usize(v: &mut RandUsize, l: &str, ui: &mut egui::Ui) { ui.label(l); ui.add(egui::DragValue::new(&mut v.value).speed(0.1)); ui.label("variance:"); ui.add(egui::DragValue::new(&mut v.randomness).speed(0.1)); ui.end_row(); }