facett-demo/tests/systemmap_liquid_shot.rs
//! **A picture of the REAL LIQUID system map** (the opt-in `SystemMap::with_liquid`),
//! rendered on a live GPU through egui_kittest's wgpu backend.
//!
//! This is deliberately **not** the containment proof — that lives in
//! `facett-graph3d/src/pipes.rs` and tumbles the map through 360 orientations at 97 %
//! charge, checking every painted liquid vertex against its own tube. A single
//! screenshot confirms one camera at one instant and would never visit the orientation
//! where a containment bug shows. See `facett/.nornir/systemmap-fluid-reasoning.md` f7.
//!
//! What it IS: something to look at, plus the two pixel-level facts a picture *can*
//! honestly carry —
//!
//! 1. **the map drew** (many foreground pixels over the ground), and
//! 2. **tipping it pours the liquid** — two renders at the **same fixed camera and the
//! same fluid time**, differing only in `tilt`, differ in pixels. If `Tilt` stopped
//! reaching the solver, or gravity stopped being re-aimed from the camera, the two
//! frames become identical → RED.
//!
//! Run: `cargo test -p facett-demo --test systemmap_liquid_shot`.
//! The picture lands at `facett-demo/tests/snapshots/system_map_liquid.png`.
use egui_kittest::Harness;
use facett::Facet;
use facett::Theme;
use facett::graph3d::{Msg as CamMsg, SystemMap, SystemMapMsg};
use facett_demo::demo_systemmap::demo_systemmap_liquid;
/// Render a `SystemMap` to an RGBA image on the wgpu backend at a fixed size.
fn shot(mut m: SystemMap) -> image::RgbaImage {
let mut harness = Harness::builder().with_size(egui::vec2(720.0, 540.0)).build_ui(move |ui| {
facett::set_theme(ui.ctx(), Theme::nordic_aurora());
m.ui(ui);
});
harness.run_steps(4);
harness.render().expect("wgpu render produced an image")
}
/// Per-pixel luminance (0..255) of an RGBA image.
fn luma(px: &image::Rgba<u8>) -> u16 {
(px[0] as u16 * 54 + px[1] as u16 * 183 + px[2] as u16 * 19) >> 8
}
/// The liquid map at a **pinned camera** (spin off, yaw fixed), tipped by `tilt` and
/// settled for a fixed number of host-supplied steps. Two of these differ **only** in
/// the tilt — no clock is read on this path, so they are otherwise identical.
fn tipped(tilt: f32, steps: usize) -> SystemMap {
let mut m = demo_systemmap_liquid();
let _ = m.update(SystemMapMsg::Camera(CamMsg::SetSpin(false)));
let _ = m.update(SystemMapMsg::Camera(CamMsg::SetYaw(0.6)));
let _ = m.update(SystemMapMsg::SetTilt(tilt));
for _ in 0..steps {
m.tick_fluid(1.0 / 60.0);
}
m
}
#[test]
fn liquid_system_map_renders_and_tipping_it_pours() {
// Same camera, same number of steps, same seed — only the tilt differs.
let level = tipped(0.0, 240);
let over = tipped(1.6, 240);
// The invariants still hold in the exact states being photographed.
for (name, m) in [("level", &level), ("tipped", &over)] {
let j = m.state_json();
let l = &j["liquid"];
assert!(l["max_fill"].as_f64().unwrap() <= 1.0, "{name}: nothing left the pipes");
assert!(l["total_volume"].as_f64().unwrap() > 1.0, "{name}: there is liquid");
}
let img_a = shot(level);
let img_b = shot(over);
assert_eq!(img_a.dimensions(), img_b.dimensions(), "same-size frames");
let (w, h) = img_a.dimensions();
assert!(w >= 700 && h >= 520, "rendered at the requested size ({w}x{h})");
// ── 1. NON-BLANK ────────────────────────────────────────────────────────────────
let bg_l = luma(img_a.get_pixel(3, 3)) as i32;
let foreground = img_a.pixels().filter(|px| (luma(px) as i32 - bg_l).abs() > 8).count();
assert!(foreground > 1_500, "the liquid map drew (foreground px={foreground}) — not blank");
// ── 2. TIPPING IT POURS ─────────────────────────────────────────────────────────
let changed = img_a
.pixels()
.zip(img_b.pixels())
.filter(|(a, b)| {
(a[0] as i32 - b[0] as i32).abs() + (a[1] as i32 - b[1] as i32).abs() + (a[2] as i32 - b[2] as i32).abs() > 16
})
.count();
assert!(
changed > 200,
"tipping the map moved the liquid (changed px={changed}) — RED if Tilt stops reaching the solver"
);
// Save the tipped frame — it is the one that shows liquid lying in the troughs.
let out_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots");
std::fs::create_dir_all(&out_dir).unwrap();
let path = out_dir.join("system_map_liquid.png");
image::DynamicImage::ImageRgba8(img_b).save(&path).expect("save PNG");
let bytes = std::fs::metadata(&path).unwrap().len();
assert!(bytes > 5_000, "the picture must be non-trivial (got {bytes} bytes)");
eprintln!(
"liquid system-map picture → {} ({bytes} bytes) · foreground={foreground} poured={changed}",
path.display()
);
}