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/pick_rides_the_painted_rect.rs

//! **A click picks the ground under the CURSOR** — the pixel ray shot through the rect
//! this pane actually painted into, not through one the host reconstructed.
//!
//! # Why this file exists
//!
//! `ground_lonlat_at_px` takes an offset from the pane centre and a pane size, so the
//! CALLER has to know the rect `Map3D::view` allocated. korp did not, and could not —
//! only `view` knows what it allocated — and it got the rect wrong twice:
//!
//! * `ui.min_rect()`, which in korp's `Ui` is the view STRIP ∪ the map, so every ray
//!   shot ~half a strip too high (fixed korp `7227855`, 2026-08-20);
//! * its replacement, the cursor position read before and after the paint, which is
//!   exact at the top and `item_spacing.y` too low at the bottom.
//!
//! The second one is MEASURED, not inferred: on korp's own pane at 1280×900 the host's
//! rect came out **3.00 px taller with its centre 1.50 px below** the rect this crate
//! painted. Both errors bias the ray the same way — it aims HIGH — and a pixel ray is a
//! lever: near the horizon one pixel of aim is tens of metres of ground. That is
//! The owner's *"markeringen hamnar LÅÅÅNG bort i y-led"*.
//!
//! So `ground_lonlat_at_screen` takes the raw pointer position and answers it against
//! `painted_rect()`. This file pins that it does, and — the half that matters — pins
//! that the OLD reconstruction is measurably wrong, so the guard has been seen red on
//! the exact defect it was written for. A test that only checked the round trip would
//! pass just as happily on the broken code, because the broken code round-trips fine
//! *against its own wrong rect*.

mod common;

use egui_kittest::Harness;
use facett_core::Facet as _;
use facett_map3d::Map3D;
use facett_map3d::extrude::geo_distance_m;

/// Central Stockholm — the centre `common::town` builds around.
const STOCKHOLM: (f64, f64) = (18.0686, 59.3293);
/// A town a kilometre across: big enough that a metre of pick error is a real number,
/// small enough to stay one scene.
const SPAN_M: f64 = 1_000.0;
/// korp's own pane size, so the pixel numbers here are the pixel numbers there.
const PANE: egui::Vec2 = egui::Vec2 { x: 1280.0, y: 900.0 };

/// The rider's seat: a few hundred metres up, nose down, heading north — the fågelläge
/// regime, where the ray is shallow and the lever is long.
const EYE_ALT_M: f64 = 220.0;
const PITCH_RAD: f64 = -0.30;

/// A painted pane holding a real geographic scene, posed at the rider's seat.
///
/// The pose is written BETWEEN frames and a frame is run after it, because
/// `painted_rect` is a paint cache: it is the rect of a frame that happened, and a pose
/// nothing has painted yet has no rect to be picked against.
fn flown() -> Harness<'static, Map3D> {
    let ways = common::town(SPAN_M);
    let m = Map3D::from_geometry("pick-rect", &ways);
    assert!(m.geo_bbox().is_some(), "precondition: the scene has a geographic span");
    let mut h = Harness::builder().with_size(PANE).build_ui_state(
        |ui, s: &mut Map3D| {
            facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
            s.ui(ui);
        },
        m,
    );
    h.run_steps(2);
    // Heading north is yaw +90° (yaw 0 points at +easting), nose down.
    assert!(
        h.state_mut().look_from_lonlat(
            STOCKHOLM.0,
            STOCKHOLM.1,
            EYE_ALT_M,
            std::f64::consts::FRAC_PI_2,
            PITCH_RAD,
            0.0,
        ),
        "a scene with a geo bbox must accept a geographic pose",
    );
    h.run_steps(2);
    h
}

/// The ONE writer of the flat-Earth ruler these fixtures are built on: the extruder's
/// own [`facett_map3d::extrude::M_PER_DEG_LAT`] / [`facett_map3d::extrude::m_per_deg_lon`].
/// A fixture built on a private copy of 111_320 measures a DIFFERENT earth from the code
/// under test the moment either side is edited — and the copies agree until then, which is
/// exactly how this survived in nine files (`granska-20`).
use facett_map3d::extrude::{M_PER_DEG_LAT, m_per_deg_lon};

/// A ground point `north_m` / `east_m` from the eye, and the ABSOLUTE screen position it
/// projects to through the frame that was actually painted.
///
/// This is the honest direction to build the test in: start from a place on the ground,
/// find the pixel it is under, and then ask what is under that pixel. Starting from a
/// pixel and unprojecting it would be asking the code under test for both halves.
fn ground_and_its_pixel(h: &Harness<'_, Map3D>, north_m: f64, east_m: f64) -> ((f64, f64), egui::Pos2) {
    let lon = STOCKHOLM.0 + east_m / m_per_deg_lon(STOCKHOLM.1);
    let lat = STOCKHOLM.1 + north_m / M_PER_DEG_LAT;
    let [dx, dz] = h.state().design_from_lonlat(lon, lat).expect("placeable");
    let rect = h.state().painted_rect().expect("the pane has painted");
    let p = h.state().render_camera().view().project_world(
        glam::Vec3::new(dx, 0.0, dz),
        (0.0, 0.0),
        rect.height() * 0.5,
    );
    assert!(p.visible, "the fixture must aim at ground that is ON screen, not behind the eye");
    ((lon, lat), rect.center() + egui::vec2(p.x, p.y))
}

/// **GREEN: the pick lands where the cursor is**, to well under a metre.
#[test]
fn a_screen_pick_lands_on_the_ground_under_the_cursor() {
    let h = flown();
    let mut worst: f64 = 0.0;
    for &(n, e) in &[(300.0, 0.0), (500.0, 120.0), (900.0, -250.0), (150.0, 60.0)] {
        let (want, pos) = ground_and_its_pixel(&h, n, e);
        let got = h
            .state()
            .ground_lonlat_at_screen(pos)
            .expect("a ground pixel resolves to ground");
        let err = geo_distance_m(want, got);
        worst = worst.max(err);
        assert!(
            err < 1.0,
            "the pick must land under the cursor: N{n} E{e} → {err:.2} m off \
             (want {want:?}, got {got:?})",
        );
    }
    eprintln!("PICK worst round-trip error over the fixture: {worst:.3} m");
}

/// **RED: the rect korp used to reconstruct gives a measurably different answer.**
///
/// This is the half that makes the green half mean something. It replays the OLD
/// mapping — the same absolute cursor position, resolved against a rect
/// `item_spacing.y` too tall (so its centre sits half that low), which is exactly what
/// `next_widget_position()` after the paint handed korp — and requires the answer to be
/// wrong by metres. If this ever stops firing, the two rects have become the same thing
/// and `ground_lonlat_at_screen` is no longer buying anything.
///
/// It also pins the SIGN, which is the whole reported symptom: the reconstructed rect
/// aims the ray high, so the ground point lands FURTHER north — LÅÅÅNG bort i y-led.
#[test]
fn the_reconstructed_rect_aims_long_which_is_the_defect() {
    let h = flown();
    /// egui's default vertical item spacing — the exact amount korp's rect overshot by.
    const ITEM_SPACING_Y: f32 = 3.0;

    let rect = h.state().painted_rect().expect("the pane has painted");
    // korp's old rect: same top-left, bottom pushed down by one item spacing.
    let wrong = egui::Rect::from_min_max(rect.min, rect.max + egui::vec2(0.0, ITEM_SPACING_Y));
    assert!(
        (wrong.center().y - rect.center().y - ITEM_SPACING_Y / 2.0).abs() < 1e-4,
        "the reconstruction moves the centre by half the spacing — that is the mechanism",
    );

    let mut worst_m: f64 = 0.0;
    let mut all_long = true;
    for &(n, e) in &[(300.0, 0.0), (500.0, 120.0), (900.0, -250.0), (2000.0, 0.0), (3000.0, 0.0)] {
        let (want, pos) = ground_and_its_pixel(&h, n, e);
        let c = wrong.center();
        let old = h
            .state()
            .ground_lonlat_at_px((pos.x - c.x, pos.y - c.y), (wrong.width(), wrong.height()))
            .expect("the old mapping still answers, it just answers wrong");
        let err = geo_distance_m(want, old);
        worst_m = worst_m.max(err);
        // The eye faces north, so "long" is further north than the truth.
        all_long &= old.1 > want.1;
        eprintln!(
            "RECONSTRUCTED-RECT PICK N{n} E{e}: {err:.1} m off, \
             dlat {:+.1} m",
            (old.1 - want.1) * M_PER_DEG_LAT,
        );
    }
    // MEASURED on this fixture (1 km town, eye 220 m, nose down 17°): the error grows
    // with range as the ray flattens — 1.3 m at 300 m, 4.3 m at 900 m, 12.0 m at 2 km,
    // 21.6 m at 3 km, which is the last range still on screen at this pane's ~27° FOV.
    // 15 m is a floor under the far end, not a target: it fires on the lever, which is
    // the property, while leaving room for the fixture to be re-tuned.
    assert!(
        worst_m > 15.0,
        "the reconstructed rect must be measurably wrong or this guard is blind: \
         worst {worst_m:.2} m",
    );
    assert!(all_long, "and it must err LONG — that is the reported symptom's direction");
    eprintln!("RECONSTRUCTED-RECT worst error: {worst_m:.1} m over the sweep");
}

/// **Before the first paint there is no frame to shoot through, and it says so.**
///
/// The refusal is the honest answer: a pick resolved against a rect that was never
/// painted would be a fabricated ground point, which is the class of bug this whole
/// seam exists to remove.
#[test]
fn an_unpainted_pane_refuses_the_pick_instead_of_inventing_one() {
    let m = Map3D::from_geometry("never-painted", &common::town(SPAN_M));
    assert!(m.painted_rect().is_none(), "nothing has painted yet");
    assert!(
        m.ground_lonlat_at_screen(egui::pos2(640.0, 500.0)).is_none(),
        "an unpainted pane must refuse a pick, not answer one",
    );
}