feat(ui): add sections.rs — Camera/BlackHole/Quality cards

Two-column Grid rows with unit suffixes + log scales on wide ranges.
Bloom threshold/strength hidden when Bloom=Off (spec §4.3). steps and
render_scale migrated here from the deleted Renderer section, fixing
the duplicate-render_scale bug.
This commit is contained in:
xfy 2026-07-17 15:49:42 +08:00
parent dfa8c77ab1
commit 34fa609a7d
2 changed files with 120 additions and 0 deletions

View File

@ -1,3 +1,4 @@
mod sections;
mod style;
pub mod preset;

119
src/ui/sections.rs Normal file
View File

@ -0,0 +1,119 @@
//! Per-group section render functions. Each takes the `&mut egui::Ui` it
//! draws into plus the params/camera refs it needs. The `ui_system`
//! orchestrator (mod.rs) calls them inside `group()` / `collapsing()`.
use bevy_egui::egui;
use crate::camera::OrbitCamera;
use crate::params::{
AaQuality, BlackHoleParams, BloomQuality,
};
use crate::ui::style::MUTED_TEXT;
use std::f32::consts::PI;
/// Shared two-column row helper: label + sized widget.
fn row(
ui: &mut egui::Ui,
label: &str,
add_widget: impl FnOnce(&mut egui::Ui) -> egui::Response,
) {
ui.label(label);
add_widget(ui);
ui.end_row();
}
// ============================ Always-open cards ============================
pub fn section_camera(ui: &mut egui::Ui, cam: &mut OrbitCamera) {
egui::Grid::new("camera_grid").num_columns(2).spacing([8.0, 4.0])
.show(ui, |ui| {
row(ui, "Distance", |ui| ui.add_sized([140.0, 16.0],
egui::Slider::new(&mut cam.distance, 3.0..=200.0)
.suffix(" r_g").fixed_decimals(1)));
row(ui, "Yaw", |ui| ui.add_sized([140.0, 16.0],
egui::Slider::new(&mut cam.yaw, -PI..=PI)
.suffix(" rad").fixed_decimals(2)));
row(ui, "Pitch", |ui| ui.add_sized([140.0, 16.0],
egui::Slider::new(&mut cam.pitch, (-PI + 0.05)..=(PI - 0.05))
.suffix(" rad").fixed_decimals(2)));
row(ui, "FOV", |ui| ui.add_sized([140.0, 16.0],
egui::Slider::new(&mut cam.fov, 0.3..=2.0).fixed_decimals(2)));
});
}
pub fn section_black_hole(ui: &mut egui::Ui, params: &mut BlackHoleParams) {
egui::Grid::new("bh_grid").num_columns(2).spacing([8.0, 4.0])
.show(ui, |ui| {
row(ui, "Spin (χ)", |ui| ui.add_sized([140.0, 16.0],
egui::Slider::new(&mut params.spin, 0.0..=1.0).fixed_decimals(2)));
// Read-only derived values: right column, muted color.
ui.label(egui::RichText::new("ISCO (disk inner)").color(MUTED_TEXT));
ui.label(egui::RichText::new(format!("{:.3}", crate::physics::kerr_isco(params.spin)))
.color(MUTED_TEXT));
ui.end_row();
ui.label(egui::RichText::new("Horizon r+").color(MUTED_TEXT));
ui.label(egui::RichText::new(format!("{:.3}", crate::physics::kerr_horizon(params.spin)))
.color(MUTED_TEXT));
ui.end_row();
});
}
pub fn section_quality(ui: &mut egui::Ui, params: &mut BlackHoleParams) {
// Bloom quality combobox + threshold/strength (hidden when Off — §4.3).
{
let mut q = params.bloom_quality;
egui::Grid::new("bloom_grid").num_columns(2).spacing([8.0, 4.0])
.show(ui, |ui| {
ui.label("Bloom");
egui::ComboBox::from_id_salt("bloom_combo")
.selected_text(format!("{:?}", q))
.show_ui(ui, |ui| {
ui.selectable_value(&mut q, BloomQuality::Off, "Off");
ui.selectable_value(&mut q, BloomQuality::Low, "Low");
ui.selectable_value(&mut q, BloomQuality::Medium, "Medium");
ui.selectable_value(&mut q, BloomQuality::High, "High");
});
ui.end_row();
});
params.bloom_quality = q;
}
if params.bloom_quality != BloomQuality::Off {
egui::Grid::new("bloom_detail_grid").num_columns(2).spacing([8.0, 4.0])
.show(ui, |ui| {
row(ui, "Threshold", |ui| ui.add_sized([140.0, 16.0],
egui::Slider::new(&mut params.bloom_threshold, 0.0..=3.0).fixed_decimals(2)));
row(ui, "Strength", |ui| ui.add_sized([140.0, 16.0],
egui::Slider::new(&mut params.bloom_strength, 0.0..=2.0).fixed_decimals(2)));
});
}
egui::Grid::new("renderer_grid").num_columns(2).spacing([8.0, 4.0])
.show(ui, |ui| {
// steps + render_scale migrated here from the deleted Renderer section.
row(ui, "Steps", |ui| ui.add_sized([140.0, 16.0],
egui::Slider::new(&mut params.steps, 50..=600)
.logarithmic(true).fixed_decimals(0)));
row(ui, "Resolution", |ui| ui.add_sized([140.0, 16.0],
egui::Slider::new(&mut params.render_scale, 0.25..=1.0)
.logarithmic(true).fixed_decimals(2)));
row(ui, "Exposure", |ui| ui.add_sized([140.0, 16.0],
egui::Slider::new(&mut params.exposure, 0.5..=3.0).fixed_decimals(2)));
row(ui, "Star AA", |ui| ui.checkbox(&mut params.star_aa, ""));
// Ring anti-alias (supersampling for lensed-image rings).
let mut a = params.aa_quality;
ui.label("Ring AA");
egui::ComboBox::from_id_salt("aa_combo")
.selected_text(format!("{:?} ({}×)", a, a.samples()))
.show_ui(ui, |ui| {
ui.selectable_value(&mut a, AaQuality::Off, "Off (1×)");
ui.selectable_value(&mut a, AaQuality::Low, "Low (2×)");
ui.selectable_value(&mut a, AaQuality::High, "High (4×)");
});
ui.end_row();
params.aa_quality = a;
});
ui.label(
egui::RichText::new("MSAA is decorative on a fullscreen shader (no geometry edges).")
.small().color(MUTED_TEXT),
);
}