feat(ui): add preset module with hash-based Custom detection
Preset::Cinematic/Performance/Web write 5 params (steps, render_scale,
bloom/disk/aa quality); Custom is a no-op marker. params_hash hashes
ONLY preset-touched fields so non-preset edits (disk_tilt, etc.) don't
spuriously flip the bar to Custom. Test-pinned via tests/preset_test.rs.
Exports ui + params from lib.rs so the test can construct params.
Lib-compile unblock: the library crate could not build with only
`pub mod {params,physics,ui}` because src/ui/mod.rs's ui_system
references crate::camera and crate::scene (transitively crate::render).
Added pub mod {camera,scene,render,web} to lib.rs and reworked main.rs
to consume them via `use singularity_rs::*` instead of redeclaring
the same file-level modules. preset is declared `pub mod` (not private
`mod`) because tests/preset_test.rs imports it cross-crate.
This commit is contained in:
parent
800d51f8c9
commit
3bf1f1ac83
11
src/lib.rs
11
src/lib.rs
@ -1 +1,12 @@
|
||||
// Lib crate root. The bin (main.rs) consumes these via `singularity_rs::...`
|
||||
// rather than redeclaring the modules itself; otherwise both crate roots
|
||||
// would try to own the same file-level modules (compile error: E0428/
|
||||
// E0152). This is why every module the binary needs lives here.
|
||||
pub mod camera;
|
||||
pub mod params;
|
||||
pub mod physics;
|
||||
pub mod render;
|
||||
pub mod scene;
|
||||
pub mod ui;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod web;
|
||||
|
||||
14
src/main.rs
14
src/main.rs
@ -1,14 +1,12 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::WindowPlugin;
|
||||
|
||||
// The bin consumes the lib crate's modules; it does NOT redeclare them.
|
||||
// src/lib.rs owns every file-level module (camera/params/physics/render/
|
||||
// scene/ui/web), so we import what main() needs from the lib root.
|
||||
use singularity_rs::render::BlackHolePlugin;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web;
|
||||
mod render;
|
||||
mod camera;
|
||||
mod params;
|
||||
mod ui;
|
||||
mod scene;
|
||||
mod physics;
|
||||
use singularity_rs::web;
|
||||
|
||||
fn main() {
|
||||
// On web, abort startup if WebGPU isn't available and show a message.
|
||||
@ -50,6 +48,6 @@ fn main() {
|
||||
// Dropping it removes that noise and shrinks the wasm binary.
|
||||
.disable::<bevy::audio::AudioPlugin>(),
|
||||
)
|
||||
.add_plugins(render::BlackHolePlugin)
|
||||
.add_plugins(BlackHolePlugin)
|
||||
.run();
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
mod style;
|
||||
pub mod preset;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_egui::egui;
|
||||
|
||||
103
src/ui/preset.rs
Normal file
103
src/ui/preset.rs
Normal file
@ -0,0 +1,103 @@
|
||||
//! Preset bundles + Custom-edit detection (design spec §3).
|
||||
//!
|
||||
//! `params_hash` hashes ONLY the fields that presets touch — otherwise a
|
||||
//! non-preset edit (e.g. disk_tilt) would spuriously flip the bar to Custom.
|
||||
//! The field set below is the single source of truth; if you add a field to
|
||||
//! a preset bundle, add it to `hashed_fields` too or the test
|
||||
//! `non_preset_field_change_flips_to_custom` will not catch the leak.
|
||||
|
||||
use crate::params::{AaQuality, BlackHoleParams, BloomQuality, DiskQuality};
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Preset {
|
||||
Cinematic,
|
||||
Performance,
|
||||
Web,
|
||||
/// Read-only marker set when any preset-touched field is hand-edited
|
||||
/// away from a preset bundle. `apply(Custom, _)` is a no-op.
|
||||
Custom,
|
||||
}
|
||||
|
||||
/// The exact params each preset writes. `Custom` writes nothing.
|
||||
fn bundle(p: Preset) -> Option<HashedParams> {
|
||||
// Defaults chosen to mirror cfg!(wasm32) dual-defaults already in the codebase:
|
||||
// Cinematic = desktop default, Web = wasm default, Performance = a low tier.
|
||||
Some(match p {
|
||||
Preset::Cinematic => HashedParams {
|
||||
steps: 300, render_scale: 0.75,
|
||||
bloom_quality: BloomQuality::High, disk_quality: DiskQuality::High, aa_quality: AaQuality::High,
|
||||
},
|
||||
Preset::Performance => HashedParams {
|
||||
steps: 150, render_scale: 0.5,
|
||||
bloom_quality: BloomQuality::Low, disk_quality: DiskQuality::Low, aa_quality: AaQuality::Off,
|
||||
},
|
||||
Preset::Web => HashedParams {
|
||||
steps: 200, render_scale: 0.5,
|
||||
bloom_quality: BloomQuality::Low, disk_quality: DiskQuality::Low, aa_quality: AaQuality::Off,
|
||||
},
|
||||
Preset::Custom => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply a preset's bundle to params. `Custom` is a no-op.
|
||||
pub fn apply(p: Preset, params: &mut BlackHoleParams) {
|
||||
if let Some(b) = bundle(p) {
|
||||
params.steps = b.steps;
|
||||
params.render_scale = b.render_scale;
|
||||
params.bloom_quality = b.bloom_quality;
|
||||
params.disk_quality = b.disk_quality;
|
||||
params.aa_quality = b.aa_quality;
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable hash of a preset's canonical bundle. `Custom` returns 0 (it never
|
||||
/// matches any real params state, by construction — see `hashed_fields`).
|
||||
pub fn canonical_hash(p: Preset) -> u64 {
|
||||
match bundle(p) {
|
||||
Some(b) => b.hash(),
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash of the preset-touched fields of a live params. Used by `ui_system`
|
||||
/// to detect hand-edits and flip the bar to Custom.
|
||||
pub fn params_hash(params: &BlackHoleParams) -> u64 {
|
||||
let h = HashedParams {
|
||||
steps: params.steps,
|
||||
render_scale: params.render_scale,
|
||||
bloom_quality: params.bloom_quality,
|
||||
disk_quality: params.disk_quality,
|
||||
aa_quality: params.aa_quality,
|
||||
};
|
||||
h.hash()
|
||||
}
|
||||
|
||||
// --- internals ---
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct HashedParams {
|
||||
steps: u32,
|
||||
render_scale: f32,
|
||||
bloom_quality: BloomQuality,
|
||||
disk_quality: DiskQuality,
|
||||
aa_quality: AaQuality,
|
||||
}
|
||||
|
||||
impl HashedParams {
|
||||
fn hash(&self) -> u64 {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
self.steps.hash(&mut hasher);
|
||||
self.render_scale.to_bits().hash(&mut hasher); // f32: hash bit pattern, not value
|
||||
self.bloom_quality.as_u32_().hash(&mut hasher);
|
||||
self.disk_quality.as_u32().hash(&mut hasher);
|
||||
self.aa_quality.samples().hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// Local trait adapters: BloomQuality has levels(), DiskQuality has as_u32(),
|
||||
// AaQuality has samples(). Unify under one name for hashing.
|
||||
trait AsU32ForHash { fn as_u32_(self) -> u32; }
|
||||
impl AsU32ForHash for BloomQuality { fn as_u32_(self) -> u32 { self.levels() } }
|
||||
41
tests/preset_test.rs
Normal file
41
tests/preset_test.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use singularity_rs::params::BlackHoleParams;
|
||||
use singularity_rs::ui::preset::{Preset, apply, canonical_hash, params_hash};
|
||||
|
||||
#[test]
|
||||
fn canonical_hash_matches_just_applied_params() {
|
||||
// After applying a preset, params_hash of the result must equal that
|
||||
// preset's canonical_hash — otherwise the Custom-detection logic would
|
||||
// immediately flip a freshly-applied preset back to Custom.
|
||||
let mut p = BlackHoleParams::default();
|
||||
for preset in [Preset::Cinematic, Preset::Performance, Preset::Web] {
|
||||
apply(preset, &mut p);
|
||||
assert_eq!(
|
||||
canonical_hash(preset),
|
||||
params_hash(&p),
|
||||
"preset {:?}: apply() did not reproduce canonical_hash",
|
||||
preset
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_preset_field_change_flips_to_custom() {
|
||||
// Editing a field that NO preset touches (disk_tilt is not in any preset bundle)
|
||||
// must NOT change params_hash. This guards the "hash only preset fields"
|
||||
// invariant: non-preset edits must not spuriously flip to Custom.
|
||||
let mut p = BlackHoleParams::default();
|
||||
let h0 = params_hash(&p);
|
||||
p.disk_tilt = 1.0; // not in any preset bundle
|
||||
assert_eq!(h0, params_hash(&p), "non-preset field leaked into hash");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preset_field_change_differs_from_canonical() {
|
||||
// Editing a preset-touched field after applying must change the hash
|
||||
// away from the preset's canonical_hash (i.e. flip to Custom).
|
||||
let mut p = BlackHoleParams::default();
|
||||
apply(Preset::Cinematic, &mut p);
|
||||
let h_canonical = canonical_hash(Preset::Cinematic);
|
||||
p.steps = 299; // off by one from the Cinematic bundle
|
||||
assert_ne!(h_canonical, params_hash(&p));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user