use crate::{ particle_editor::{hooks::hooks_plugin, spawn::spawn_plugin}, particles::{LifetimeCurve, ParticleEffect, RandF32, RandUsize, RandVec2}, }; use crate::prelude::*; use bevy_egui::{EguiContexts, EguiPlugin, EguiPrimaryContextPass, egui}; use ordered_float::OrderedFloat; use std::collections::BTreeMap; mod ecs; mod hooks; mod spawn; pub fn particle_editor_plugin(app: &mut App) { app.add_plugins(DefaultPlugins); app.insert_resource(Gravity::ZERO); app.add_plugins(EguiPlugin::default()); app.add_plugins(PhysicsPlugins::default().with_length_unit(100.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, *u.1)) .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, ); editor_resource.scale_curve.push(new_v); } ui.end_row(); effect.scale = LifetimeCurve( editor_resource .scale_curve .iter() .copied() .collect::>(), ); 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, ); editor_resource.color_curve.push(new_v); } ui.end_row(); let curve_copied = editor_resource.color_curve.clone(); effect.color = LifetimeCurve( curve_copied .iter() .map(|(u, v)| { ( *u, Color::Srgba(Srgba::new( f32::from(v[0]) / 256.0, f32::from(v[1]) / 256.0, f32::from(v[2]) / 256.0, f32::from(v[3]) / 256.0, )), ) }) .collect::>(), ); ui.separator(); ui.end_row(); }); ui.horizontal(|ui| { if ui.button("Generate").clicked() { effect.scale = LifetimeCurve( editor_resource .scale_curve .iter() .copied() .collect::>(), ); let curve_copied = editor_resource.color_curve.clone(); effect.color = LifetimeCurve( curve_copied .iter() .map(|(u, v)| { ( *u, Color::Srgba(Srgba::new( f32::from(v[0]) / 256.0, f32::from(v[1]) / 256.0, f32::from(v[2]) / 256.0, f32::from(v[3]) / 256.0, )), ) }) .collect::>(), ); 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, *u.1)) .collect::>(); editor_resource.color_curve = effect .color .0 .iter() .map(|u| { (*u.0, { 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(); }