Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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)",
    );
}