From 72e873af5317268d2c36d89604f622f12181eaba Mon Sep 17 00:00:00 2001 From: core Date: Sun, 6 Jul 2025 22:37:45 -0400 Subject: [PATCH] feat?: the worst particle editor of all time --- Cargo.lock | 20 ++- crates/unified/Cargo.toml | 1 + crates/unified/src/particle_editor/mod.rs | 200 +++++++++++++++++++++- crates/unified/src/particles.rs | 2 +- 4 files changed, 213 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d6360425b355a76b9958d17dfef372988838742..70fb7f398344d0ba6da3b15ba076f8ae058542fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -818,7 +818,7 @@ dependencies = [ "futures-lite", "js-sys", "parking_lot", - "ron", + "ron 0.8.1", "serde", "thiserror 1.0.69", "wasm-bindgen", @@ -856,7 +856,7 @@ dependencies = [ "js-sys", "notify-debouncer-full", "parking_lot", - "ron", + "ron 0.8.1", "serde", "stackfuture", "thiserror 2.0.12", @@ -1217,7 +1217,7 @@ checksum = "740289c63cb752adbad3823c49c01d479d853a10d76ccfe5ce3d1f3ea8e14ac2" dependencies = [ "bevy 0.16.1", "rand 0.8.5", - "ron", + "ron 0.8.1", "serde", ] @@ -7208,6 +7208,19 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "ron" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" +dependencies = [ + "base64 0.22.1", + "bitflags 2.9.1", + "serde", + "serde_derive", + "unicode-ident", +] + [[package]] name = "roxmltree" version = "0.20.0" @@ -7765,6 +7778,7 @@ dependencies = [ "getrandom 0.3.3", "ordered-float 5.0.0", "rand 0.9.1", + "ron 0.10.1", "serde", "tracing-subscriber", "tracing-wasm", diff --git a/crates/unified/Cargo.toml b/crates/unified/Cargo.toml index d170fdce8343fafdf0a3c03a414c24930f01d558..b8023893366b3a1f32c441b42f0166ac06cc96b0 100644 --- a/crates/unified/Cargo.toml +++ b/crates/unified/Cargo.toml @@ -49,6 +49,7 @@ aeronet_transport = "0.14" bevy_egui = "0.35" ordered-float = { version = "5", features = ["serde"] } +ron = "0.10" bevy_enoki = "0.4" diff --git a/crates/unified/src/particle_editor/mod.rs b/crates/unified/src/particle_editor/mod.rs index a6ce33835566152a3386e727b513f84e10e5f286..66fae5e654a069544723c5b6f8c752fca766a959 100644 --- a/crates/unified/src/particle_editor/mod.rs +++ b/crates/unified/src/particle_editor/mod.rs @@ -1,12 +1,29 @@ +use std::collections::BTreeMap; use bevy::prelude::*; -use bevy_egui::{EguiPlugin, EguiPrimaryContextPass}; +use bevy_egui::{egui, EguiContexts, EguiPlugin, EguiPrimaryContextPass}; +use ordered_float::OrderedFloat; +use ron::ser::PrettyConfig; use crate::particles::{LifetimeCurve, ParticleEffect, RandF32, RandUsize, RandVec2}; pub fn particle_editor_plugin(app: &mut App) { app.add_plugins(DefaultPlugins); app.add_plugins(EguiPlugin::default()); + app.add_systems(Startup, setup_camera_system); app.add_systems(EguiPrimaryContextPass, editor_ui); app.add_systems(Startup, setup_editor_effect); + 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) { @@ -32,13 +49,184 @@ fn setup_editor_effect(mut commands: Commands) { ]), color: LifetimeCurve::new(&[ (0.0f32, Srgba::new(1.0, 0.0, 0.0, 1.0).into()), - (0.5, Srgba::new(0.0, 1.0, 0.0, 1.0).into()), - (1.0, Srgba::new(0.0, 0.0, 1.0, 1.0).into()), - (1.5, Srgba::new(1.0, 0.0, 1.0, 1.0).into()), - (2.0, Srgba::new(1.0, 1.0, 1.0, 1.0).into()) ]), } )); } -fn editor_ui() {} \ No newline at end of file +fn setup_camera_system(mut commands: Commands) { + commands.spawn(Camera2d); +} + +#[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)); + 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)); + 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(); +} \ No newline at end of file diff --git a/crates/unified/src/particles.rs b/crates/unified/src/particles.rs index e48118b8dfb05e64f973efad8251199a84180b91..7e02653b1c7977b5a557d5dd1d755f69e7e121f7 100644 --- a/crates/unified/src/particles.rs +++ b/crates/unified/src/particles.rs @@ -38,7 +38,7 @@ pub struct ParticleEffect { } #[derive(Serialize, Deserialize)] -pub struct LifetimeCurve(BTreeMap, P>); +pub struct LifetimeCurve(pub BTreeMap, P>); impl LifetimeCurve

{ pub fn new<'a>(points: impl IntoIterator) -> Self where P: 'a { Self(BTreeMap::from_iter(points.into_iter().map(|u| (OrderedFloat(u.0), u.1.clone()))))