facett-core 0.1.19

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **The collapse, proven.** These tests are the argument that one core can
//! serve all seven use cases: the *same* [`plan`] call, swapping only the three
//! traits, produces a map frame, a graph frame, a 3D frame, and — in one frame —
//! both at once.

use super::*;
use crate::legibility::FrameOpts;
use egui::{Pos2, Rect, Vec2};

/// The Mercator projection, written here **in the test** rather than in the
/// library — `facett-core` must not learn what a latitude is. This is exactly
/// the three lines `facett-geomap` will pass to [`Projected::new`].
fn mercator(lon: f64, lat: f64) -> (f64, f64) {
    let x = (lon + 180.0) / 360.0;
    let lat_r = lat.to_radians();
    let y = (1.0 - (lat_r.tan() + 1.0 / lat_r.cos()).ln() / std::f64::consts::PI) / 2.0;
    (x, y)
}

fn viewport() -> Rect {
    Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0))
}

/// Sweden-ish sample: Malmö, Göteborg, Stockholm, Umeå, Kiruna.
fn swedish_cities() -> Vec<[f64; 2]> {
    vec![
        [13.0007, 55.6050], // Malmö
        [11.9746, 57.7089], // Göteborg
        [18.0686, 59.3293], // Stockholm
        [20.2630, 63.8258], // Umeå
        [20.2253, 67.8558], // Kiruna
    ]
}

// ─────────────────────────────────────────────────────────────────────────────
// Use case 1 — OSM 2D: Mercator position, flat, zoom bands
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn uc1_osm_2d_mercator_flat() {
    let pos = Projected::new(swedish_cities(), "mercator", mercator);
    let elev = Flat;
    let hier = Flatten::new(pos.feature_count());
    let layer = LayerRef::new(1, &pos, &elev, &hier);

    // Centre on Stockholm, ~2 Mercator-units-per... i.e. a country view.
    let (sx, sy) = mercator(18.0686, 59.3293);
    let view = View {
        centre: WorldPos::flat(sx, sy),
        scale: 6000.0,
        viewport: viewport(),
        epoch: 1,
    };
    let mut cache = EngineCache::default();
    let plan = plan(&[layer], &view, FrameOpts::default(), &mut cache);

    assert_eq!(plan.layers.len(), 1);
    assert!(plan.visible() > 0, "a country view must see cities");
    // Stockholm sits at the eye, so it lands at the viewport centre.
    let l = &plan.layers[0];
    let i = l.visible.iter().position(|&f| f == 2).expect("Stockholm culled out of its own view");
    let c = view.viewport.center();
    assert!((l.screen[i] - c).length() < 0.01, "the eye feature must land at the viewport centre");
    // Mercator y grows southward: Kiruna (north) must be ABOVE Malmö (south).
    assert!(plan.layers[0].elevation.iter().all(|&z| z == 0.0), "use case 1 is flat");
}

#[test]
fn uc1_mercator_north_is_up_and_the_engine_did_not_flip_it() {
    let pos = Projected::new(swedish_cities(), "mercator", mercator);
    let (elev, hier) = (Flat, Flatten::new(5));
    let layer = LayerRef::new(1, &pos, &elev, &hier);
    let view = View { centre: WorldPos::flat(0.55, 0.28), scale: 2000.0, viewport: viewport(), epoch: 1 };
    let mut cache = EngineCache::default();
    let plan = plan(&[layer], &view, FrameOpts::default(), &mut cache);
    let l = &plan.layers[0];
    let y_of = |f: u32| l.visible.iter().position(|&x| x == f).map(|i| l.screen[i].y);
    if let (Some(kiruna), Some(malmo)) = (y_of(4), y_of(0)) {
        assert!(kiruna < malmo, "Kiruna (67.8N) must be above Malmö (55.6N): {kiruna} vs {malmo}");
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Use case 2 — OSM 3D: the SAME map, one trait swapped
// ─────────────────────────────────────────────────────────────────────────────

/// ★ This is the `facett-map3d` collapse in one assertion: 3D is 2D with a
/// different [`ElevationSource`]. Nothing else in the call changes.
#[test]
fn uc2_osm_3d_is_uc1_with_one_trait_swapped() {
    let pos = Projected::new(swedish_cities(), "mercator", mercator);
    let hier = Flatten::new(5);
    let view = View { centre: WorldPos::flat(0.55, 0.28), scale: 2000.0, viewport: viewport(), epoch: 1 };

    let flat = Flat;
    let mut c1 = EngineCache::default();
    let p2d = plan(&[LayerRef::new(1, &pos, &flat, &hier)], &view, FrameOpts::default(), &mut c1);

    // A terrain heightfield — a pure (x, y) -> z function, supplied by the
    // caller. `facett-core` still knows nothing about DEMs.
    let terrain = Sampled::new("terrain", (0.0, 500.0), |x, y| ((x * 97.0).sin() + (y * 61.0).cos()) * 250.0 + 250.0);
    let mut c2 = EngineCache::default();
    let p3d = plan(&[LayerRef::new(1, &pos, &terrain, &hier)], &view, FrameOpts::default(), &mut c2);

    assert_eq!(p2d.layers[0].visible, p3d.layers[0].visible, "the same features must be visible");
    assert_eq!(p2d.layers[0].screen, p3d.layers[0].screen, "ground-plane projection must be identical");
    assert!(p2d.layers[0].elevation.iter().all(|&z| z == 0.0));
    assert!(
        p3d.layers[0].elevation.iter().any(|&z| z > 0.0),
        "the terrain source must actually raise something — otherwise the test proves nothing"
    );
}

// ─────────────────────────────────────────────────────────────────────────────
// Use case 3 — flat 2D overlay: Identity
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn uc3_flat_overlay_identity_is_screen_space() {
    let pos = Identity::from_f32(&[(100.0, 100.0), (400.0, 300.0), (700.0, 500.0)]);
    let (elev, hier) = (Flat, Flatten::new(3));
    // scale 1.0 + centre at the viewport centre = identity screen space.
    let view = View {
        centre: WorldPos::flat(400.0, 300.0),
        scale: 1.0,
        viewport: viewport(),
        epoch: 1,
    };
    let mut cache = EngineCache::default();
    let plan = plan(&[LayerRef::new(1, &pos, &elev, &hier)], &view, FrameOpts::default(), &mut cache);
    let l = &plan.layers[0];
    for (i, &f) in l.visible.iter().enumerate() {
        let want = match f {
            0 => Pos2::new(100.0, 100.0),
            1 => Pos2::new(400.0, 300.0),
            _ => Pos2::new(700.0, 500.0),
        };
        assert!((l.screen[i] - want).length() < 0.001, "identity source must be pixel-for-pixel");
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Use cases 4 + 5 — graph 2D and graph 3D
// ─────────────────────────────────────────────────────────────────────────────

fn ring_layout(n: usize, r: f64) -> Vec<[f64; 2]> {
    (0..n)
        .map(|i| {
            let a = i as f64 / n as f64 * std::f64::consts::TAU;
            [a.cos() * r, a.sin() * r]
        })
        .collect()
}

#[test]
fn uc4_graph_2d_layout_flat_with_edges_and_labels() {
    let pos = Layout::new(ring_layout(60, 200.0));
    let (elev, hier) = (Flat, Flatten::new(60));
    let edges: Vec<(u32, u32)> = (0..60u32).map(|i| (i, (i + 1) % 60)).collect();
    let sizes = vec![Vec2::new(40.0, 12.0); 60];
    let prio: Vec<f32> = (0..60).map(|i| i as f32).collect();
    let layer = LayerRef {
        edges: &edges,
        label_size: &sizes,
        label_priority: &prio,
        ..LayerRef::new(1, &pos, &elev, &hier)
    };
    let view = View { centre: WorldPos::flat(0.0, 0.0), scale: 1.5, viewport: viewport(), epoch: 1 };
    let mut cache = EngineCache::default();
    let p = plan(&[layer], &view, FrameOpts::legible(), &mut cache);

    assert_eq!(p.layers[0].visible.len(), 60, "the whole ring fits the viewport");
    let labels = p.frame.labels.as_ref().expect("legible() must produce a label placement");
    assert!(labels.placed.len() > 0, "some labels must be placed");
    assert!(p.frame.report.edges_considered > 0, "edges must reach the thinner");
}

#[test]
fn uc5_graph_3d_is_uc4_with_a_metric_elevation() {
    let pos = Layout::new(ring_layout(60, 200.0));
    let hier = Flatten::new(60);
    let view = View { centre: WorldPos::flat(0.0, 0.0), scale: 1.5, viewport: viewport(), epoch: 1 };

    let flat = Flat;
    let mut c1 = EngineCache::default();
    let p2 = plan(&[LayerRef::new(1, &pos, &flat, &hier)], &view, FrameOpts::default(), &mut c1);

    // Height IS betweenness centrality / risk — use case 5 and 6's Z axis.
    let centrality: Vec<f32> = (0..60).map(|i| (i as f32 % 7.0) * 3.0).collect();
    let metric = Metric::new(centrality, 2.0);
    assert_eq!(metric.z_range(), (0.0, 36.0), "the metric must declare its real range");
    let mut c2 = EngineCache::default();
    let p3 = plan(&[LayerRef::new(1, &pos, &metric, &hier)], &view, FrameOpts::default(), &mut c2);

    assert_eq!(p2.layers[0].screen, p3.layers[0].screen, "ground projection unchanged by elevation");
    assert!(p3.layers[0].elevation.iter().any(|&z| z > 0.0));
}

// ─────────────────────────────────────────────────────────────────────────────
// Use case 6 — Mímir code graph: drill repo→crate→file→symbol via Hierarchy
// ─────────────────────────────────────────────────────────────────────────────

/// ★ The key idea, asserted: a graph cluster-expand IS a map zoom. The same
/// `Bands` type expresses both, and `level_for(scale)` is the only control.
#[test]
fn uc6_hierarchy_drill_is_the_same_operation_as_a_map_zoom() {
    // 0: one repo. 1: three crates. 2: nine files.
    let bands = Bands::new(
        vec![
            Band { members: vec![0], min_scale: 0.0, children: [(0u32, vec![1, 2, 3])].into_iter().collect() },
            Band {
                members: vec![1, 2, 3],
                min_scale: 10.0,
                children: [(1u32, vec![4, 5, 6]), (2, vec![7, 8, 9]), (3, vec![10, 11, 12])].into_iter().collect(),
            },
            Band { members: (4..=12).collect(), min_scale: 100.0, children: Default::default() },
        ],
        "mimir-code",
    );
    assert_eq!(bands.depth(), 3);
    assert_eq!(bands.level_for(1.0), 0, "zoomed out = the repo node");
    assert_eq!(bands.level_for(50.0), 1, "mid = the crates");
    assert_eq!(bands.level_for(500.0), 2, "zoomed in = the files");
    assert_eq!(bands.expand(0, 0), &[1, 2, 3], "one step deeper swaps coarse for fine");
    // Node 5 lives at level 2; its stand-in one level up is crate node 1.
    assert_eq!(bands.parent_of(2, 5), Some(1), "zooming out must carry the selection up");

    // And it drives the engine: the SAME view, three scales, three member sets.
    let pts: Vec<[f64; 2]> = (0..13).map(|i| [(i % 4) as f64 * 50.0, (i / 4) as f64 * 50.0]).collect();
    let pos = Layout::new(pts);
    let risk: Vec<f32> = (0..13).map(|i| (i as f32) * 0.5).collect();
    let elev = Metric::new(risk, 4.0);
    let mut counts = Vec::new();
    for scale in [1.0, 50.0, 500.0] {
        let view = View {
            centre: WorldPos::flat(75.0, 75.0),
            scale,
            viewport: Rect::from_min_size(Pos2::ZERO, Vec2::new(100_000.0, 100_000.0)),
            epoch: 1,
        };
        let mut cache = EngineCache::default();
        let p = plan(&[LayerRef::new(1, &pos, &elev, &bands)], &view, FrameOpts::default(), &mut cache);
        counts.push((p.layers[0].level, p.layers[0].visible.len()));
    }
    assert_eq!(counts, vec![(0, 1), (1, 3), (2, 9)], "descending a level must swap coarse instances for fine");
}

// ─────────────────────────────────────────────────────────────────────────────
// ★ Use case 7 — a graph drawn ON geography: BOTH position sources, one frame
// ─────────────────────────────────────────────────────────────────────────────

/// The reason it must be one engine. Two layers, two *different*
/// [`PositionSource`] implementations, one [`View`], one label plan, one pick
/// index. If this compiles and passes, the abstraction is not bent.
#[test]
fn uc7_graph_over_map_two_position_sources_in_one_frame() {
    // Layer 1: the basemap, in Mercator.
    let map_pos = Projected::new(swedish_cities(), "mercator", mercator);
    let map_elev = Flat;
    let map_hier = Flatten::new(5);

    // Layer 2: an infra graph whose nodes are ALSO geographic, plus edges.
    // (A datacentre graph: Malmö→Stockholm→Umeå.)
    let g_pos = Projected::new(
        vec![[13.0007, 55.6050], [18.0686, 59.3293], [20.2630, 63.8258]],
        "mercator",
        mercator,
    );
    let g_elev = Metric::new(vec![10.0, 40.0, 25.0], 1.0);
    let g_hier = Flatten::new(3);
    let g_edges = [(0u32, 1u32), (1, 2)];

    let view = View { centre: WorldPos::flat(0.55, 0.28), scale: 3000.0, viewport: viewport(), epoch: 1 };
    let mut cache = EngineCache::default();
    let p = plan(
        &[
            LayerRef::new(1, &map_pos, &map_elev, &map_hier),
            LayerRef { edges: &g_edges, pick_shape: PickShape::Radius(14.0), ..LayerRef::new(2, &g_pos, &g_elev, &g_hier) },
        ],
        &view,
        FrameOpts::legible(),
        &mut cache,
    );

    assert_eq!(p.layers.len(), 2, "both layers must survive the frame");
    assert_eq!(p.pick.layer_count(), 2, "one pick index, both layers in it");
    assert!(p.layers[1].elevation.iter().any(|&z| z > 0.0), "the graph layer keeps its metric height");
    assert!(p.layers[0].elevation.iter().all(|&z| z == 0.0), "the map layer stays flat");

    // The two sources agree in world space: Stockholm is Stockholm in both.
    let map_sthlm = p.layers[0]
        .visible
        .iter()
        .position(|&f| f == 2)
        .map(|i| p.layers[0].screen[i])
        .expect("map layer lost Stockholm");
    let graph_sthlm = p.layers[1]
        .visible
        .iter()
        .position(|&f| f == 1)
        .map(|i| p.layers[1].screen[i])
        .expect("graph layer lost Stockholm");
    assert!(
        (map_sthlm - graph_sthlm).length() < 0.01,
        "a graph node ON geography must land on its city: {map_sthlm:?} vs {graph_sthlm:?}"
    );

    // The graph layer is on top, so it wins the click.
    let hit = p.pick.pick(graph_sthlm);
    assert_eq!(hit.id.layer(), 2, "the upper layer must win the contested pixel");
    assert_eq!(hit.id.feature(), 1);
}

// ─────────────────────────────────────────────────────────────────────────────
// Cross-cutting properties
// ─────────────────────────────────────────────────────────────────────────────

/// RED-PROVEN: delete the `visible.contains` guard in `plan` and this fails —
/// an oversize feature would be emitted twice and picked twice.
#[test]
fn oversize_features_are_forced_in_exactly_once() {
    #[derive(Debug)]
    struct Wide(Vec<[f64; 2]>);
    impl PositionSource for Wide {
        fn feature_count(&self) -> usize {
            self.0.len()
        }
        fn position(&self, id: u32) -> WorldPos {
            self.0.get(id as usize).map_or(WorldPos::new(f64::NAN, f64::NAN, f64::NAN), |&[x, y]| WorldPos::flat(x, y))
        }
        fn bounds(&self, id: u32) -> WorldAabb {
            // Feature 0 spans the whole dataset — a motorway / a country border.
            if id == 0 {
                WorldAabb { min: WorldPos::flat(0.0, 0.0), max: WorldPos::flat(1000.0, 1000.0) }
            } else {
                WorldAabb::point(self.position(id))
            }
        }
        fn kind(&self) -> &'static str {
            "wide"
        }
    }
    let pos = Wide((0..200).map(|i| [i as f64 * 5.0, i as f64 * 5.0]).collect());
    let (elev, hier) = (Flat, Flatten::new(200));
    let view = View { centre: WorldPos::flat(500.0, 500.0), scale: 1.0, viewport: viewport(), epoch: 1 };
    let mut cache = EngineCache::default();
    let p = plan(&[LayerRef::new(1, &pos, &elev, &hier)], &view, FrameOpts::default(), &mut cache);
    let l = &p.layers[0];
    assert!(l.visible.contains(&0), "the dataset-spanning feature must be drawn");
    assert_eq!(l.visible.iter().filter(|&&f| f == 0).count(), 1, "and exactly once");
}

#[test]
fn the_cull_index_is_reused_across_frames_when_nothing_moved() {
    let pos = Layout::new(ring_layout(500, 300.0));
    let (elev, hier) = (Flat, Flatten::new(500));
    let mut cache = EngineCache::default();
    for epoch in 0..5u64 {
        let view = View { centre: WorldPos::flat(0.0, 0.0), scale: 1.0, viewport: viewport(), epoch };
        let _ = plan(&[LayerRef::new(1, &pos, &elev, &hier)], &view, FrameOpts::default(), &mut cache);
    }
    assert_eq!(cache.len(), 1, "a static layer must build its index once, not once per frame");
}

#[test]
fn empty_everything_plans_a_frame_without_panicking() {
    let pos = Identity::new(Vec::new());
    let (elev, hier) = (Flat, Flatten::new(0));
    let view = View::default();
    let mut cache = EngineCache::default();
    let p = plan(&[LayerRef::new(1, &pos, &elev, &hier)], &view, FrameOpts::legible(), &mut cache);
    assert_eq!(p.visible(), 0);
    assert!(p.pick.pick(Pos2::ZERO).id.is_nothing());
}

#[test]
fn state_json_publishes_the_observables_a_robot_asserts_on() {
    let pos = Layout::new(ring_layout(20, 100.0));
    let (elev, hier) = (Flat, Flatten::new(20));
    let view = View { centre: WorldPos::flat(0.0, 0.0), scale: 2.0, viewport: viewport(), epoch: 3 };
    let mut cache = EngineCache::default();
    let p = plan(&[LayerRef::new(1, &pos, &elev, &hier)], &view, FrameOpts::legible(), &mut cache);
    let j = p.state_json();
    assert_eq!(j["layers"][0]["visible"], 20);
    assert_eq!(j["pick_layers"], 1);
    assert!(j["layers"][0]["cull"]["candidates"].is_number());
}