proof-engine 0.2.1

Real-time graphics from math: glyphs and particles moved by ODEs, strange attractors and force fields, drawn with HDR bloom on OpenGL.
Documentation

// ── Particle Editor UI State ──────────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct ParticleEditorState {
    pub selected_emitter: Option<u32>,
    pub selected_effect: Option<u32>,
    pub viewport_camera_pos: Vec3,
    pub viewport_camera_rot: Quat,
    pub preview_playing: bool,
    pub preview_time: f32,
    pub show_grid: bool,
    pub grid_size: f32,
    pub background_color: Vec4,
    pub zoom_level: f32,
    pub panel_sizes: HashMap<String, f32>,
    pub undo_stack: VecDeque<String>,
    pub redo_stack: Vec<String>,
    pub max_undo: usize,
    pub modified: bool,
}

impl ParticleEditorState {
    pub fn new() -> Self {
        Self {
            selected_emitter: None, selected_effect: None,
            viewport_camera_pos: Vec3::new(0.0, 2.0, 5.0),
            viewport_camera_rot: Quat::IDENTITY,
            preview_playing: false, preview_time: 0.0,
            show_grid: true, grid_size: 1.0,
            background_color: Vec4::new(0.1, 0.1, 0.15, 1.0),
            zoom_level: 1.0,
            panel_sizes: HashMap::new(),
            undo_stack: VecDeque::new(), redo_stack: Vec::new(),
            max_undo: 100, modified: false,
        }
    }
    pub fn play(&mut self) { self.preview_playing = true; }
    pub fn stop(&mut self) { self.preview_playing = false; self.preview_time = 0.0; }
    pub fn pause(&mut self) { self.preview_playing = false; }
    pub fn tick_preview(&mut self, dt: f32) { if self.preview_playing { self.preview_time += dt; } }
    pub fn select_emitter(&mut self, id: u32) { self.selected_emitter = Some(id); }
    pub fn deselect(&mut self) { self.selected_emitter = None; }
    pub fn push_undo(&mut self, desc: impl Into<String>) {
        if self.undo_stack.len() >= self.max_undo { self.undo_stack.pop_front(); }
        self.undo_stack.push_back(desc.into()); self.redo_stack.clear(); self.modified = true;
    }
    pub fn undo(&mut self) -> Option<String> { let v = self.undo_stack.pop_back()?; self.redo_stack.push(v.clone()); Some(v) }
    pub fn redo(&mut self) -> Option<String> { let v = self.redo_stack.pop()?; self.undo_stack.push_back(v.clone()); Some(v) }
    pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() }
    pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
    pub fn mark_saved(&mut self) { self.modified = false; }
}

impl Default for ParticleEditorState {
    fn default() -> Self { Self::new() }
}

// ── Particle Effect Exporter ──────────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct ParticleExportSettings {
    pub format: String,
    pub include_textures: bool,
    pub compress: bool,
    pub min_emit_rate: Option<f32>,
    pub bake_curves: bool,
    pub target_platform: String,
    pub output_dir: String,
}

impl ParticleExportSettings {
    pub fn new(format: impl Into<String>, output_dir: impl Into<String>) -> Self {
        Self { format: format.into(), include_textures: true, compress: false, min_emit_rate: None, bake_curves: false, target_platform: "pc".into(), output_dir: output_dir.into() }
    }
    pub fn compressed(mut self) -> Self { self.compress = true; self }
    pub fn baked(mut self) -> Self { self.bake_curves = true; self }
    pub fn for_platform(mut self, platform: impl Into<String>) -> Self { self.target_platform = platform.into(); self }
}

#[derive(Clone, Debug)]
pub struct ParticleExportResult {
    pub success: bool,
    pub files_written: Vec<String>,
    pub errors: Vec<String>,
    pub warnings: Vec<String>,
    pub total_bytes: u64,
    pub duration_ms: f32,
}

impl ParticleExportResult {
    pub fn ok(files: Vec<String>, bytes: u64) -> Self {
        Self { success: true, files_written: files, errors: Vec::new(), warnings: Vec::new(), total_bytes: bytes, duration_ms: 0.0 }
    }
    pub fn err(msg: impl Into<String>) -> Self {
        Self { success: false, files_written: Vec::new(), errors: vec![msg.into()], warnings: Vec::new(), total_bytes: 0, duration_ms: 0.0 }
    }
    pub fn add_warning(&mut self, w: impl Into<String>) { self.warnings.push(w.into()); }
    pub fn file_count(&self) -> usize { self.files_written.len() }
}

// ── Particle System Benchmark ─────────────────────────────────────────────────

#[derive(Clone, Debug, Default)]
pub struct ParticleBenchmarkResult {
    pub scenario_name: String,
    pub particle_count: u32,
    pub emitter_count: u32,
    pub avg_fps: f32,
    pub min_fps: f32,
    pub max_fps: f32,
    pub avg_sim_ms: f32,
    pub avg_render_ms: f32,
    pub frame_count: u64,
}

impl ParticleBenchmarkResult {
    pub fn new(name: impl Into<String>) -> Self { Self { scenario_name: name.into(), min_fps: f32::MAX, ..Default::default() } }
    pub fn record_frame(&mut self, fps: f32, sim_ms: f32, render_ms: f32) {
        if fps < self.min_fps { self.min_fps = fps; }
        if fps > self.max_fps { self.max_fps = fps; }
        self.avg_fps = (self.avg_fps * self.frame_count as f32 + fps) / (self.frame_count + 1) as f32;
        self.avg_sim_ms = (self.avg_sim_ms * self.frame_count as f32 + sim_ms) / (self.frame_count + 1) as f32;
        self.avg_render_ms = (self.avg_render_ms * self.frame_count as f32 + render_ms) / (self.frame_count + 1) as f32;
        self.frame_count += 1;
    }
    pub fn total_ms(&self) -> f32 { self.avg_sim_ms + self.avg_render_ms }
    pub fn passed_60fps(&self) -> bool { self.avg_fps >= 60.0 }
    pub fn grade(&self) -> &'static str {
        if self.avg_fps >= 120.0 { "Excellent" } else if self.avg_fps >= 60.0 { "Good" } else if self.avg_fps >= 30.0 { "Acceptable" } else { "Poor" }
    }
}

// ── Particle Simulation Scenarios ────────────────────────────────────────────

#[derive(Clone, Debug)]
pub struct ParticleScenario {
    pub name: String,
    pub description: String,
    pub emitter_configs: Vec<String>,
    pub duration_secs: f32,
    pub expected_peak_particles: u32,
    pub benchmark: Option<ParticleBenchmarkResult>,
}

impl ParticleScenario {
    pub fn new(name: impl Into<String>, desc: impl Into<String>) -> Self {
        Self { name: name.into(), description: desc.into(), emitter_configs: Vec::new(), duration_secs: 10.0, expected_peak_particles: 1000, benchmark: None }
    }
    pub fn add_emitter_config(&mut self, config: impl Into<String>) { self.emitter_configs.push(config.into()); }
    pub fn set_expected_peak(&mut self, n: u32) { self.expected_peak_particles = n; }
    pub fn has_benchmark(&self) -> bool { self.benchmark.is_some() }
    pub fn benchmark_grade(&self) -> Option<&'static str> { self.benchmark.as_ref().map(|b| b.grade()) }
}

pub fn build_stress_test_scenarios() -> Vec<ParticleScenario> {
    let mut s1 = ParticleScenario::new("Low Load", "Single small emitter");
    s1.set_expected_peak(500);
    let mut s2 = ParticleScenario::new("Medium Load", "Multiple emitters");
    s2.set_expected_peak(10000);
    let mut s3 = ParticleScenario::new("High Load", "Many emitters with trails");
    s3.set_expected_peak(100000);
    let mut s4 = ParticleScenario::new("Extreme Load", "Maximum particle count stress test");
    s4.set_expected_peak(500000);
    vec![s1, s2, s3, s4]
}

// ── Final particle constants ──────────────────────────────────────────────────

pub const PARTICLE_EDITOR_UNDO_MAX: usize = 100;
pub const PARTICLE_EXPORT_FORMATS: &[&str] = &["json", "binary", "xml", "custom"];
pub const PARTICLE_BENCH_FRAME_MIN: u64 = 300;
pub const PARTICLE_SCENARIO_MAX: usize = 16;
pub const PARTICLE_EMITTER_GRID_DEFAULT_SIZE: f32 = 1.0;
pub const PARTICLE_VIEWPORT_FAR_PLANE: f32 = 1000.0;
pub const PARTICLE_VIEWPORT_NEAR_PLANE: f32 = 0.01;

pub fn particle_editor_full_info() -> String {
    format!(
        "ParticleSystemEditor — {} feature modules, editor + runtime + benchmark pipeline",
        particle_module_count()
    )
}