~starkingdoms/starkingdoms

72e873af5317268d2c36d89604f622f12181eaba — core 5 months ago f2ba6eb
feat?: the worst particle editor of all time
M Cargo.lock => Cargo.lock +17 -3
@@ 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",
]



@@ 7209,6 7209,19 @@ dependencies = [
]

[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 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",

M crates/unified/Cargo.toml => crates/unified/Cargo.toml +1 -0
@@ 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"


M crates/unified/src/particle_editor/mod.rs => crates/unified/src/particle_editor/mod.rs +194 -6
@@ 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::<Vec<_>>(),
        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>, f32)>,
    color_curve: Vec<(OrderedFloat<f32>, [u8; 4])>
}


fn editor_ui(mut contexts: EguiContexts, effect: Single<&mut ParticleEffect>, mut editor_resource: ResMut<EditorResource>) -> 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::<Vec<_>>();
                        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::<Vec<_>>();
                    },
                    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

M crates/unified/src/particles.rs => crates/unified/src/particles.rs +1 -1
@@ 38,7 38,7 @@ pub struct ParticleEffect {
}

#[derive(Serialize, Deserialize)]
pub struct LifetimeCurve<P: Lerp>(BTreeMap<OrderedFloat<f32>, P>);
pub struct LifetimeCurve<P: Lerp>(pub BTreeMap<OrderedFloat<f32>, P>);
impl<P: Lerp + Copy> LifetimeCurve<P> {
    pub fn new<'a>(points: impl IntoIterator<Item = &'a (f32, P)>) -> Self where P: 'a {
        Self(BTreeMap::from_iter(points.into_iter().map(|u| (OrderedFloat(u.0), u.1.clone()))))