facett-core 0.1.7

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **CPU SDF coverage + AA-line raster** — the L0 fallback that produces the same
//! pixels as the GPU pipeline ([`super::super::gpu::sdf_pipeline`]).
//!
//! This module is the **source of truth** for the signed-distance coverage math:
//! the WGSL fragment shaders (`sdf.wgsl` / `line.wgsl`) evaluate the *same*
//! functions, so a CPU and a GPU frame match (the `sdf_primitives` parity test
//! pins this). It is pure, allocation-free coverage math — the compositing into a
//! pixmap lives in [`super::CpuCanvas`].
//!
//! ## The coverage convention
//! Every shape returns a coverage `α ∈ [0, 1]`: `1` fully inside, `0` fully
//! outside, a smooth ramp across the AA band `[−aa, +aa]` around each edge. The
//! ramp is `smoothstep`, matching the GPU `smoothstep`, so the AA fringe is
//! identical. With `aa = 0` the edge is hard (a step at the boundary).

use crate::render::prim::{shape, LineInstance, QuadInstance};

/// `smoothstep(edge0, edge1, x)` — the cubic Hermite ramp, identical to WGSL's
/// `smoothstep`. Used to turn a signed distance into AA coverage.
#[inline]
pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
    if edge0 == edge1 {
        return if x < edge0 { 0.0 } else { 1.0 };
    }
    let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
    t * t * (3.0 - 2.0 * t)
}

/// Coverage from a **signed distance** `d` (negative = inside) and an AA half-band
/// `aa`: `1` deep inside, ramping to `0` across `[−aa, +aa]`. This is the single
/// distance→coverage kernel every shape funnels through (the GPU does the same:
/// `1 - smoothstep(-aa, aa, d)`).
#[inline]
pub fn coverage_from_sd(d: f32, aa: f32) -> f32 {
    if aa <= 0.0 {
        return if d <= 0.0 { 1.0 } else { 0.0 };
    }
    1.0 - smoothstep(-aa, aa, d)
}

/// Filled-disc coverage at a point `dist` pixels from the centre. `α = 1` inside
/// `radius − aa`, ramps to `0` by `radius + aa`.
#[inline]
pub fn circle_coverage(dist: f32, radius: f32, aa: f32) -> f32 {
    coverage_from_sd(dist - radius, aa)
}

/// Annulus (ring) coverage: inside the outer edge **and** outside the inner edge.
/// Combines the disc SDF (`dist − outer`) with the hole SDF (`inner − dist`); the
/// max of the two signed distances is the ring's SDF (intersection of "inside
/// outer" and "outside inner").
#[inline]
pub fn ring_coverage(dist: f32, outer: f32, inner: f32, aa: f32) -> f32 {
    let sd = (dist - outer).max(inner - dist);
    coverage_from_sd(sd, aa)
}

/// Rounded-axis-aligned-square coverage. `(px, py)` is the offset from the centre;
/// `half` the half-extent; `corner` the corner radius. The classic rounded-box
/// SDF: `length(max(|p| − (half − corner), 0)) − corner`.
#[inline]
pub fn square_coverage(px: f32, py: f32, half: f32, corner: f32, aa: f32) -> f32 {
    let corner = corner.clamp(0.0, half);
    // Box-SDF with interior term: d = |p| - (half - corner).
    let dx = px.abs() - (half - corner);
    let dy = py.abs() - (half - corner);
    let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
    let inside = dx.max(dy).min(0.0); // negative when fully inside the box
    let sd = outside + inside - corner;
    coverage_from_sd(sd, aa)
}

/// Diamond (square rotated 45°) coverage: the L1 / Manhattan ball.
/// `sd = (|x| + |y|) − half`.
#[inline]
pub fn diamond_coverage(px: f32, py: f32, half: f32, aa: f32) -> f32 {
    let sd = (px.abs() + py.abs()) - half;
    coverage_from_sd(sd, aa)
}

/// Upward triangle coverage as the intersection of three half-planes (bottom + two
/// slanted sides). Each edge contributes a signed distance; the max is the convex
/// polygon SDF (negative inside). Apex at `(0, −half)`, base corners at
/// `(±half, +half)`. Robust + cheap — and identical to `sdf.wgsl`'s triangle.
#[inline]
pub fn triangle_coverage(px: f32, py: f32, half: f32, aa: f32) -> f32 {
    // Bottom edge: y = +half, inside is y < half ⇒ sd = py - half.
    let bottom = py - half;
    // Right edge from apex (0,-half) to (half, half): direction (half, 2*half) ~ (1,2).
    // Outward normal (2, -1) normalized; line through apex.
    let inv = 1.0 / (5.0f32).sqrt(); // |(2,-1)| = sqrt(5)
    let right = (px * 2.0 - (py + half)) * inv;
    // Left edge from apex to (-half, half): mirror, outward normal (-2,-1).
    let left = (px * -2.0 - (py + half)) * inv;
    let sd = bottom.max(right).max(left);
    coverage_from_sd(sd, aa)
}

/// Evaluate a [`QuadInstance`]'s coverage at the pixel-centre offset `(dx, dy)`
/// from the instance centre. The single dispatch the CPU raster calls per pixel —
/// it mirrors `sdf.wgsl`'s `coverage()` switch.
#[inline]
pub fn quad_coverage(inst: &QuadInstance, dx: f32, dy: f32) -> f32 {
    match inst.shape {
        shape::CIRCLE => circle_coverage((dx * dx + dy * dy).sqrt(), inst.radius, inst.aa),
        shape::RING => ring_coverage((dx * dx + dy * dy).sqrt(), inst.radius, inst.inner, inst.aa),
        shape::SQUARE => square_coverage(dx, dy, inst.radius, inst.inner, inst.aa),
        shape::TRIANGLE => triangle_coverage(dx, dy, inst.radius, inst.aa),
        shape::DIAMOND => diamond_coverage(dx, dy, inst.radius, inst.aa),
        _ => 0.0,
    }
}

/// Squared distance from point `p` to the segment `a → b`, plus the clamped
/// parameter `t` (0 at `a`, 1 at `b`) — the line raster needs both (the `t` tells
/// it whether a butt cap rejects the pixel past an endpoint).
#[inline]
pub fn point_segment(p: [f32; 2], a: [f32; 2], b: [f32; 2]) -> (f32, f32) {
    let abx = b[0] - a[0];
    let aby = b[1] - a[1];
    let apx = p[0] - a[0];
    let apy = p[1] - a[1];
    let len2 = abx * abx + aby * aby;
    let t = if len2 <= 1e-12 { 0.0 } else { ((apx * abx + apy * aby) / len2).clamp(0.0, 1.0) };
    let cx = a[0] + abx * t;
    let cy = a[1] + aby * t;
    let dx = p[0] - cx;
    let dy = p[1] - cy;
    (dx * dx + dy * dy, t)
}

/// Thick-AA-line coverage at pixel `p` for a [`LineInstance`]. Distance to the
/// segment vs `half_width`, with the AA ramp — the **same** capsule SDF the GPU
/// `line.wgsl` evaluates. A **butt** cap additionally rejects the rounded
/// overshoot past the endpoints (so the line ends flush).
#[inline]
pub fn line_coverage(inst: &LineInstance, p: [f32; 2]) -> f32 {
    let (d2, _t) = point_segment(p, inst.a, inst.b);
    let dist = d2.sqrt();
    let mut cov = coverage_from_sd(dist - inst.half_width, inst.aa);

    if inst.cap == crate::render::prim::cap::BUTT {
        // Clip the rounded ends to a flat cap: reject the part of the pixel beyond
        // the segment span along the line direction. Project p onto the (un-
        // clamped) line axis; coverage falls off past [0, len] over the AA band.
        let abx = inst.b[0] - inst.a[0];
        let aby = inst.b[1] - inst.a[1];
        let len = (abx * abx + aby * aby).sqrt();
        if len > 1e-6 {
            let ux = abx / len;
            let uy = aby / len;
            let s = (p[0] - inst.a[0]) * ux + (p[1] - inst.a[1]) * uy; // 0..len inside
            // signed distance outside the [0, len] band along the axis.
            let axis_sd = (-s).max(s - len);
            cov *= coverage_from_sd(axis_sd, inst.aa);
        }
    }
    cov
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::render::prim::{CircleInstance, LineInstance, MarkerInstance, RingInstance};

    #[test]
    fn coverage_kernel_is_monotone_across_the_band() {
        // Inside → 1, outside → 0, monotone ramp between.
        assert_eq!(coverage_from_sd(-5.0, 1.0), 1.0);
        assert_eq!(coverage_from_sd(5.0, 1.0), 0.0);
        let mut prev = 1.0;
        let mut d = -1.0;
        while d <= 1.0 {
            let c = coverage_from_sd(d, 1.0);
            assert!(c <= prev + 1e-6, "monotone non-increasing as d grows");
            prev = c;
            d += 0.1;
        }
        // aa=0 ⇒ hard step.
        assert_eq!(coverage_from_sd(-0.01, 0.0), 1.0);
        assert_eq!(coverage_from_sd(0.01, 0.0), 0.0);
    }

    #[test]
    fn circle_lit_only_inside_radius_band() {
        let r = 10.0;
        let aa = 1.0;
        assert!(circle_coverage(0.0, r, aa) > 0.99, "centre fully lit");
        assert!(circle_coverage(r - aa - 0.5, r, aa) > 0.99, "just inside fully lit");
        assert!(circle_coverage(r + aa + 0.5, r, aa) < 0.01, "outside band dark");
        assert!((circle_coverage(r, r, aa) - 0.5).abs() < 0.05, "edge ≈ half coverage");
    }

    #[test]
    fn ring_has_a_hole() {
        let outer = 12.0;
        let inner = 6.0;
        let aa = 1.0;
        assert!(ring_coverage(9.0, outer, inner, aa) > 0.99, "in the band is lit");
        assert!(ring_coverage(0.0, outer, inner, aa) < 0.01, "centre is a hole");
        assert!(ring_coverage(20.0, outer, inner, aa) < 0.01, "outside dark");
    }

    #[test]
    fn marker_shapes_differ_at_a_corner() {
        let half = 10.0;
        let aa = 0.5;
        // A point near the box corner (8,8): inside the square, outside the diamond
        // (|8|+|8|=16 > 10) and outside the triangle.
        assert!(square_coverage(8.0, 8.0, half, 0.0, aa) > 0.9, "square fills its corner");
        assert!(diamond_coverage(8.0, 8.0, half, aa) < 0.1, "diamond excludes the corner");
        // Triangle: apex up. A point high-centre is inside; a point at the top
        // corners is outside.
        assert!(triangle_coverage(0.0, 7.0, half, aa) > 0.9, "triangle base-centre inside");
        assert!(triangle_coverage(9.0, -9.0, half, aa) < 0.1, "triangle top-corner outside");
    }

    #[test]
    fn quad_coverage_dispatches_per_shape() {
        let c = CircleInstance { center: [0.0, 0.0], radius: 5.0, color: [1.0; 4], aa: 1.0 }.lower();
        assert!(quad_coverage(&c, 0.0, 0.0) > 0.99);
        assert!(quad_coverage(&c, 10.0, 0.0) < 0.01);

        let ring = RingInstance { center: [0.0; 2], radius: 8.0, inner: 4.0, color: [1.0; 4], aa: 1.0 }
            .lower();
        assert!(quad_coverage(&ring, 0.0, 0.0) < 0.01, "ring centre hole");
        assert!(quad_coverage(&ring, 6.0, 0.0) > 0.9, "ring band lit");

        let diamond = MarkerInstance {
            center: [0.0; 2],
            radius: 10.0,
            corner: 0.0,
            color: [1.0; 4],
            aa: 0.5,
            shape: shape::DIAMOND,
        }
        .lower();
        assert!(quad_coverage(&diamond, 0.0, 0.0) > 0.9);
        assert!(quad_coverage(&diamond, 9.0, 9.0) < 0.1);
    }

    #[test]
    fn line_coverage_matches_requested_width() {
        // Horizontal segment, half_width 3 → coverage out to ±3 from the axis.
        let l = LineInstance::round([10.0, 50.0], [90.0, 50.0], 3.0, 1.0, [1.0; 4]);
        assert!(line_coverage(&l, [50.0, 50.0]) > 0.99, "on axis lit");
        assert!(line_coverage(&l, [50.0, 52.0]) > 0.9, "within half_width lit");
        assert!(line_coverage(&l, [50.0, 56.0]) < 0.05, "beyond width+aa dark");
        assert!((line_coverage(&l, [50.0, 53.0]) - 0.5).abs() < 0.1, "edge ≈ half coverage");
    }

    #[test]
    fn butt_cap_ends_flush_round_cap_overshoots() {
        let butt = LineInstance::butt([20.0, 50.0], [80.0, 50.0], 4.0, 1.0, [1.0; 4]);
        let round = LineInstance::round([20.0, 50.0], [80.0, 50.0], 4.0, 1.0, [1.0; 4]);
        // Past the endpoint along the axis: round cap still covers, butt does not.
        assert!(round.cap == crate::render::prim::cap::ROUND);
        assert!(line_coverage(&round, [82.0, 50.0]) > 0.5, "round cap overshoots");
        assert!(line_coverage(&butt, [82.0, 50.0]) < 0.2, "butt cap ends flush");
        // Both cover the interior identically.
        assert!(line_coverage(&butt, [50.0, 50.0]) > 0.99);
        assert!(line_coverage(&round, [50.0, 50.0]) > 0.99);
    }
}