facett-map3d/tests/overlay_pose.rs
//! **The per-frame overlay seam lands where the pose says** (V2.5 D.3) — LAW-2 proofs
//! for [`Map3D::set_overlay`], asserted on APPLIED output: the projected screen pixels
//! of the shared painter kernel (the SAME face list `paint_cpu` rasters — see
//! `backend::project_scene_faces`), and on the wgpu lane the bytes read back off the
//! overlay's device buffer. Never a state round-trip of the pose that was set.
//!
//! # The reds, and the directions they fire in
//!
//! * **The identity-pose decoy** (the pose test's red): the overlay is first planted at
//! `OverlayPose::default()` and its projected centroid recorded. The posed overlay
//! must land somewhere ELSE, and exactly where an independent hand-spelling of the
//! transform projects it — a seam that ignores its pose renders at the decoy's
//! pixels and fails both assertions. Seen red 2026-08-12 by making `set_overlay`
//! drop the pose (verbatim in the D.2/D.3 report).
//! * **The emissive flag dropped** (the channel's red): the same two-triangle overlay
//! run once with `[0, 4]` and once with `&[]` — the emissive face renders at its
//! plain source colour only when the flag rides along; with the flag dropped BOTH
//! faces shade and the exact-colour count reads 0.
//! * **The device clear** (wgpu): `upload_overlay(None)` must zero the draw count —
//! leaving fågelläge may not leave a korp floating in the town.
use egui::{Color32, Pos2};
use facett_map3d::backend::{project_scene_faces, RenderScene, SceneFlags, Viewport};
use facett_map3d::extrude::{Mesh3, WaterMesh};
use facett_map3d::{Map3D, OverlayPose, V3};
/// The overlay body: ONE up-facing triangle in the avatar's local frame (+Z nose,
/// +Y up, +X right wing), inked an out-of-palette magenta so the projected face list
/// identifies it by colour alone.
const INK: Color32 = Color32::from_rgb(255, 0, 255);
fn overlay_tri() -> Mesh3 {
Mesh3 {
positions: vec![[0.0, 0.0, 0.30], [-0.18, 0.0, -0.18], [0.18, 0.0, -0.18]],
normals: vec![[0.0, 1.0, 0.0]; 3],
colors: vec![INK],
indices: vec![0, 1, 2],
building: vec![false; 3],
}
}
/// A deterministic scene: the synthetic fixture at a pinned clock (spin folded, so
/// `render_camera()` answers the same pose on every call).
fn fixture() -> (Map3D, egui::Rect) {
let m = Map3D::synthetic("overlay-pose").with_clock(1.0);
let rect = egui::Rect::from_min_max(egui::pos2(100.0, 80.0), egui::pos2(820.0, 700.0));
(m, rect)
}
fn scene<'a>(m: &'a Map3D, water: &'a WaterMesh, rect: egui::Rect) -> RenderScene<'a> {
RenderScene {
mesh: m.mesh(),
water,
camera: m.render_camera(),
sun: m.sun(),
viewport: Viewport::from_rect(rect),
bg: Color32::from_rgb(20, 24, 40),
flags: SceneFlags::default(),
sky_lut: None,
clock_s: 1.0,
du_per_m: m.design_units_per_metre(),
overlay: m.overlay_world(),
overlay_emissive: &[],
}
}
/// Every projected face carrying the overlay ink, with the emissive channel riding so
/// the colour survives shading verbatim (the identification trick the emissive test
/// then pins for its own sake).
fn ink_faces(m: &Map3D, rect: egui::Rect, emissive: &'static [f32]) -> Vec<(f32, [Pos2; 3], Color32)> {
let water = WaterMesh::default();
let mut s = scene(m, &water, rect);
s.overlay_emissive = emissive;
project_scene_faces(&s).into_iter().filter(|(_, _, c)| *c == INK).collect()
}
fn centroid(faces: &[(f32, [Pos2; 3], Color32)]) -> Pos2 {
let mut x = 0.0f32;
let mut y = 0.0f32;
let mut n = 0.0f32;
for (_, pts, _) in faces {
for p in pts {
x += p.x;
y += p.y;
n += 1.0;
}
}
Pos2::new(x / n.max(1.0), y / n.max(1.0))
}
/// An INDEPENDENT spelling of the pose transform (scale → roll → pitch → yaw →
/// translate), kept deliberately separate from `set_overlay`'s matrices so the two can
/// disagree — which is what makes the pixel comparison below falsifiable.
fn hand_world(p: [f32; 3], pose: &OverlayPose) -> [f32; 3] {
let s = pose.scale;
let (x, y, z) = (p[0] * s, p[1] * s, p[2] * s);
// Roll about +Z, right wing (+X) down for positive bank.
let b = pose.bank_deg.to_radians();
let (x, y) = (x * b.cos() + y * b.sin(), -x * b.sin() + y * b.cos());
// Pitch about +X, nose (+Z) up for positive pitch.
let t = pose.pitch_deg.to_radians();
let (y, z) = (y * t.cos() + z * t.sin(), -y * t.sin() + z * t.cos());
// Yaw: compass heading h, design north = -z ⇒ rotate +Z to [sin(180-h), 0, cos(180-h)].
let a = (180.0 - pose.heading_deg).to_radians();
let (x, z) = (x * a.cos() + z * a.sin(), -x * a.sin() + z * a.cos());
[x + pose.position[0], y + pose.position[1], z + pose.position[2]]
}
/// **The pose reaches the pixels.** Red first via the identity-pose decoy; then the
/// posed overlay must land where the independent hand transform projects it.
#[test]
fn an_overlay_triangle_lands_at_the_posed_screen_position() {
let (mut m, rect) = fixture();
let tri = overlay_tri();
// RED PROBE — the decoy: the identity pose, recorded. A seam that ignores its
// pose draws HERE forever.
m.set_overlay(&tri, &[], OverlayPose::default());
let decoy_faces = ink_faces(&m, rect, &[4.0]);
assert!(!decoy_faces.is_empty(), "the identity-pose overlay must project at all");
let decoy = centroid(&decoy_faces);
// The claim: a pose with position, heading, bank AND scale all non-identity.
let pose = OverlayPose {
position: [0.35, 0.04, -0.10],
heading_deg: 90.0,
pitch_deg: 8.0,
bank_deg: 30.0,
scale: 0.5,
};
m.set_overlay(&tri, &[], pose);
let faces = ink_faces(&m, rect, &[4.0]);
assert!(!faces.is_empty(), "the posed overlay must survive projection");
let posed = centroid(&faces);
let moved = ((posed.x - decoy.x).powi(2) + (posed.y - decoy.y).powi(2)).sqrt();
assert!(
moved > 10.0,
"the pose must MOVE the overlay on screen — it sits on the identity decoy \
({decoy:?} vs {posed:?}, {moved:.1} px apart): the pose is not applied",
);
// …and it lands EXACTLY where the independent hand transform projects: every
// hand-projected vertex finds a rendered vertex within 1.5 px.
let cam = m.render_camera();
let centre = (rect.center().x, rect.center().y);
let half_h = rect.height() * 0.5;
for lp in &tri.positions {
let w = hand_world(*lp, &pose);
let pr = cam.project_view(cam.view_space(V3::new(w[0], w[1], w[2])), centre, half_h);
let hit = faces.iter().any(|(_, pts, _)| {
pts.iter().any(|p| ((p.x - pr.x).powi(2) + (p.y - pr.y).powi(2)).sqrt() < 1.5)
});
assert!(
hit,
"hand-transformed vertex {lp:?} projects to ({:.1}, {:.1}) px and no rendered \
overlay vertex sits there — the seam's transform disagrees with the spec",
pr.x, pr.y,
);
}
}
/// **The emissive channel** — an emissive face renders at its plain max-brightness
/// source colour on the painter lane (no shade, no fog) beside a lit twin that does
/// not; red via the flag dropped. The HDR halo is the GPU bloom's job and is NOT
/// asserted here (see the module doc).
#[test]
fn an_emissive_face_keeps_its_plain_colour_and_the_lit_twin_shades() {
let (mut m, rect) = fixture();
// Two identical side-by-side triangles, one flagged emissive.
let two = Mesh3 {
positions: vec![
[-0.4, 0.02, 0.2],
[-0.6, 0.02, -0.2],
[-0.2, 0.02, -0.2],
[0.4, 0.02, 0.2],
[0.2, 0.02, -0.2],
[0.6, 0.02, -0.2],
],
normals: vec![[0.0, 1.0, 0.0]; 6],
colors: vec![INK, INK],
indices: vec![0, 1, 2, 3, 4, 5],
building: vec![false; 6],
};
m.set_overlay(&two, &[], OverlayPose::default());
// With the channel: exactly the flagged HALF renders verbatim-INK.
let exact = ink_faces(&m, rect, &[0.0, 4.0]);
assert!(
!exact.is_empty(),
"the emissive face must render at its plain source colour — nothing did",
);
// …and its lit twin must NOT: count the faces of the overlay that shaded away
// from INK by re-projecting with no emissive at all (the RED half: the flag
// dropped ⇒ zero verbatim faces).
let none = ink_faces(&m, rect, &[]);
assert!(
none.is_empty(),
"with the emissive flag dropped BOTH faces must shade (lit ≠ source colour); \
{} face(s) still rendered verbatim — the painter is not lighting the overlay",
none.len(),
);
}
/// **The wgpu lane's applied bytes** (feature `wgpu`): the posed world positions and
/// the emissive w land on the device, a new pose REPLACES them, and `None` clears the
/// draw count — leaving flight mode may not leave a korp floating in the town.
#[cfg(feature = "wgpu")]
#[test]
fn the_device_overlay_buffer_carries_the_pose_and_clears_on_none() {
use facett_map3d::gpu::{build_overlay_geometry, proof_wgpu_setup, Map3dRenderer};
let setup = proof_wgpu_setup().unwrap_or_else(egui_kittest::wgpu::default_wgpu_setup);
let rs = egui_kittest::wgpu::create_render_state(setup, Default::default());
let info = rs.adapter.get_info();
println!(" adapter: {} [{:?}/{:?}]", info.name, info.device_type, info.backend);
let mut r = Map3dRenderer::new(&rs.device, wgpu::TextureFormat::Rgba8UnormSrgb);
let (mut m, _) = fixture();
let tri = overlay_tri();
m.set_overlay(&tri, &[], OverlayPose::default());
let a = build_overlay_geometry(m.overlay_world().expect("set"), &[4.0]);
m.set_overlay(
&tri,
&[],
OverlayPose { position: [0.4, 0.1, 0.0], heading_deg: 45.0, ..OverlayPose::default() },
);
let b = build_overlay_geometry(m.overlay_world().expect("set"), &[4.0]);
assert_ne!(
bytemuck::cast_slice::<_, u8>(&a.vertices),
bytemuck::cast_slice::<_, u8>(&b.vertices),
"fixture sanity: the two poses must be byte-distinct",
);
// The emissive w is IN the payload: every vertex carries col.w = 1 + 4.
assert!(
a.vertices.iter().all(|v| (v.col[3] - 5.0).abs() < 1e-6),
"the emissive channel must ride the vertex colour's w as 1 + e",
);
r.upload_overlay_for_test(&rs.device, Some(&a));
let (buf, n) = r.overlay_for_test().expect("A uploaded");
assert_eq!(n, 3, "one triangle = three expanded vertices");
let dev = facett_core::render::gpu::read_buffer_range(&rs.device, &rs.queue, buf, 0, buf.size());
assert_eq!(dev, bytemuck::cast_slice::<_, u8>(&a.vertices), "the device holds pose A");
// RED direction: a seam with a first-upload latch would still hold A here.
r.upload_overlay_for_test(&rs.device, Some(&b));
let (buf, _) = r.overlay_for_test().expect("B uploaded");
let dev = facett_core::render::gpu::read_buffer_range(&rs.device, &rs.queue, buf, 0, buf.size());
assert_eq!(
dev,
bytemuck::cast_slice::<_, u8>(&b.vertices),
"a moved pose must reach the GPU every frame — these are A's bytes",
);
// Leaving flight: the body clears the SAME frame.
r.upload_overlay_for_test(&rs.device, None);
assert!(r.overlay_for_test().is_none(), "None must clear the overlay draw entirely");
}
/// **A close overlay pulls the near plane in — and the ride becomes visible**
/// (V2.5 fix round, symptom "jag ser inte korpen").
///
/// The rider's seat parks the eye METRES behind the avatar: `~1e-3` design units on
/// a city scene, far inside BOTH stock near planes (CPU clip 0.02 du; the GPU
/// matrix's tight band bottoms out at 0.05 du) — so the korp was clipped invisible
/// on every frame while pose, counts and buffer bytes all stayed green. The fix is
/// [`Map3D`]'s overlay-aware near override, and this asserts it on APPLIED output:
/// the projected face list, not the stored number.
///
/// RED directions: drop `refresh_overlay_near` from `set_overlay` and the close
/// face projects to nothing (seen red 2026-08-12 while writing this — the assert
/// below read "0 ink faces" until the refresh landed); drop the `look_from` refresh
/// and a camera that flies AFTER planting keeps a stale near.
#[test]
fn a_rider_distance_overlay_survives_the_near_plane() {
let (mut m, rect) = fixture();
let cam0 = m.render_camera();
assert!(
(cam0.near_clip() - facett_map3d::OrbitCamera::NEAR_PLANE).abs() < 1e-9,
"precondition: no overlay, stock near",
);
// Plant the body 0.004 du in front of the eye — a fifth of the stock near.
let eye = cam0.eye();
let (fwd, _, _) = cam0.basis();
let d = 0.004f32;
let pose = OverlayPose {
position: [eye.x + fwd.x * d, eye.y + fwd.y * d, eye.z + fwd.z * d],
scale: 0.02,
..OverlayPose::default()
};
m.set_overlay(&overlay_tri(), &[], pose);
let cam = m.render_camera();
assert!(
cam.near_clip() < facett_map3d::OrbitCamera::NEAR_PLANE,
"a 0.004 du body must shrink the near plane (got {})",
cam.near_clip(),
);
assert!(
cam.near_clip() < d && cam.near_clip() >= 1.0e-4,
"the override must sit between the floor and the body (got {})",
cam.near_clip(),
);
// …and the GPU matrix takes the SAME number: near_override flows through
// OrbitCamera::view into the one projection writer.
assert_eq!(
cam.view().near_override,
cam.near_override,
"the override must reach the unified view core",
);
// The applied output: the face PROJECTS. Under the stock 0.02 near this exact
// scene renders 0 ink faces (the red seen while writing this test).
let faces = ink_faces(&m, rect, &[4.0]);
assert!(
!faces.is_empty(),
"a rider-distance overlay must survive the near clip and reach the pixels",
);
// Leaving flight restores the stock band the same frame.
m.clear_overlay();
assert!(
(m.render_camera().near_clip() - facett_map3d::OrbitCamera::NEAR_PLANE).abs() < 1e-9,
"clear_overlay must restore the stock near plane",
);
}
/// **`add_overlay` appends a second body at its OWN pose** (the waypoint marker's
/// seam): the merged world mesh carries both, each where its pose says, and the
/// per-triangle emissive stays parallel across the append.
///
/// RED directions: an `add_overlay` that re-uses the first pose puts both faces on
/// one centroid (the separation assert fires); one that replaces instead of
/// appending leaves 1 face; an emissive append that forgets the pad mis-parallels
/// the channel and the emissive face count comes out wrong.
#[test]
fn add_overlay_appends_a_second_body_at_its_own_pose() {
let (mut m, rect) = fixture();
let tri = overlay_tri();
let a = OverlayPose { position: [0.3, 0.05, 0.0], ..OverlayPose::default() };
let b = OverlayPose { position: [-0.3, 0.05, 0.0], ..OverlayPose::default() };
m.set_overlay(&tri, &[], a);
m.add_overlay(&tri, &[4.0], b);
let world = m.overlay_world().expect("overlay set");
assert_eq!(world.indices.len(), 6, "two appended triangles");
assert_eq!(world.colors.len(), 2, "per-triangle colours stay parallel");
// Both flagged emissive so both keep the verbatim INK the filter identifies by
// (a lit face shades away from the ink — that is the emissive test's subject).
let faces = ink_faces(&m, rect, &[4.0, 4.0]);
assert_eq!(faces.len(), 2, "both bodies must reach the projected face list");
let c0 = centroid(&faces[..1]);
let c1 = centroid(&faces[1..]);
let apart = ((c0.x - c1.x).powi(2) + (c0.y - c1.y).powi(2)).sqrt();
assert!(
apart > 20.0,
"the second body must land at ITS pose, not the first's ({apart:.1} px apart)",
);
}