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

nordisk/facett

facett-map/src/l0_points.rs

//! **L0 SDF points/markers path** (CONS-CORE Phase C, step 2; feature `wgpu`).
//!
//! The map's **point layer** (`(lon, lat)` markers) lowered into the shared L0 SDF
//! vocabulary ([`CircleInstance`](facett_core::render::CircleInstance)) and drawn
//! through core's [`GpuSdfRenderer`](facett_core::render::gpu::GpuSdfRenderer) —
//! instanced screen-aligned quads with a fragment-shader SDF, the millions-of-points
//! GPU path the render-engine spec calls for.
//!
//! This is the **opt-in alternative** for points. facett-map's default
//! [`MapView::ui`](crate::MapView) still paints points as egui circles on the CPU
//! (the golden / render-proof path is untouched); and the **line basemap** keeps the
//! `GpuMapRenderer` cull + `draw_indirect` pipeline unchanged. This module only adds
//! an SDF points lane on top of the shared kernel.
//!
//! Coordinate contract: callers pass **already-projected screen pixels** (the map
//! fits its Mercator bbox into the rect — see `MapView::ui`'s `to_px`), so the SDF
//! instances are in the same screen space the kernel rasterizes.

use facett_core::render::{CircleInstance, Frame, QuadInstance};

/// Lower a set of **screen-pixel** point positions into L0 SDF circle instances of
/// the given pixel `radius` + straight RGBA colour. The domain-agnostic step: a map
/// vessel-point and a graph node are the same `CircleInstance` to the kernel.
pub fn lower_points(points_px: &[(f32, f32)], radius: f32, color: [f32; 4], aa: f32) -> Vec<QuadInstance> {
    points_px
        .iter()
        .map(|&(x, y)| CircleInstance { center: [x, y], radius, color, aa }.lower())
        .collect()
}

/// Render `points_px` (screen-pixel marker centres) as L0 SDF discs onto a
/// `width × height` straight-RGBA8 [`Frame`], through core's GPU SDF pipeline
/// ([`offscreen_render`](facett_core::render::gpu::offscreen_render)).
///
/// Returns `None` when no wgpu adapter is available (CPU-only / sandboxed box) — the
/// caller then keeps the default CPU point painter. This never replaces the default
/// `MapView::ui` path; it is the GPU-points lane a host can opt into.
pub fn render_points_l0(
    width: u32,
    height: u32,
    background: [f32; 4],
    points_px: &[(f32, f32)],
    radius: f32,
    color: [f32; 4],
) -> Option<Frame> {
    let quads = lower_points(points_px, radius, color, 1.0);
    facett_core::render::gpu::offscreen_render(width, height, background, &quads, &[])
}

#[cfg(test)]
mod tests {
    use super::*;
    use facett_core::render::prim::shape;

    /// INJECT-ASSERT: the point layer lowers into L0 SDF **circle** instances at the
    /// right screen centres, radius, and colour — the shared vocabulary the GPU SDF
    /// pipeline consumes. (Geometry/lowering oracle; no GPU needed.)
    #[test]
    fn points_lower_into_core_sdf_circles() {
        let pts = [(10.0, 20.0), (30.0, 40.0), (50.0, 60.0)];
        let col = [0.2, 0.7, 0.9, 1.0];
        let quads = lower_points(&pts, 1.3, col, 1.0);
        assert_eq!(quads.len(), 3, "one SDF quad per point");
        for (q, &(x, y)) in quads.iter().zip(pts.iter()) {
            assert_eq!(q.shape, shape::CIRCLE, "a map point is an SDF circle");
            assert_eq!(q.center, [x, y]);
            assert_eq!(q.radius, 1.3);
            assert!((q.color[1] - 0.7).abs() < 1e-6, "point colour carried into the quad");
            assert_eq!(q.inner, 0.0, "a plain disc has no ring hole");
        }
    }

    /// INJECT-ASSERT (GPU, graceful): when a wgpu adapter is present, the L0 SDF
    /// points path renders a **non-blank** frame through core's `GpuSdfRenderer`; on
    /// a CPU-only box `offscreen_render` returns `None` and we skip (the default CPU
    /// painter would handle points there). Either way: no panic, real output asserted.
    #[test]
    fn l0_points_render_nonblank_through_core_gpu_when_available() {
        let pts: Vec<(f32, f32)> = (0..200)
            .map(|i| (40.0 + (i % 20) as f32 * 15.0, 40.0 + (i / 20) as f32 * 30.0))
            .collect();
        let frame = render_points_l0(360, 360, [0.05, 0.06, 0.09, 1.0], &pts, 3.0, [0.9, 0.6, 0.2, 1.0]);
        match frame {
            Some(f) => {
                assert_eq!(f.rgba.len(), 360 * 360 * 4);
                assert!(f.lit_px() > 0, "L0 SDF points drew lit pixels through the core GPU pipeline");
            }
            None => {
                eprintln!("[l0-points] no wgpu adapter — CPU painter handles points (skipped)");
            }
        }
    }
}