facett-core 0.1.7

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **The L0 SDF instance model** — the domain-agnostic primitive vocabulary both
//! the CPU raster ([`super::cpu::sdf`]) and the GPU pipeline
//! ([`super::gpu::sdf_pipeline`], feature `wgpu`) draw from.
//!
//! Everything here is a flat, `bytemuck`-able POD describing **one instance** of a
//! signed-distance shape in **screen space** (pixels). The renderer expands each
//! into a screen-aligned AA quad whose fragment shader (GPU) or per-pixel coverage
//! loop (CPU) evaluates the same signed-distance function — so the two backends
//! produce matching pixels (the `sdf_primitives` parity test pins this).
//!
//! There is no map/graph here on purpose: a node, a city marker, a POI dot, and a
//! waypoint are all just a [`CircleInstance`]/[`MarkerInstance`]; a road, an edge,
//! and a leader line are all a [`LineInstance`]. The skins (Phase C) translate
//! their domain into these.
//!
//! ## Coordinate + colour contract
//! - Positions, radii, widths, and AA bands are in **screen pixels** (`f32`).
//! - Colours are linear-ish straight `[r, g, b, a]` in `[0, 1]` — exactly what
//!   [`super::gpu::types::color32_to_f32`] produces, so a `Color32` flows through
//!   unchanged.
//! - `aa` is the **half-width of the anti-alias band** in pixels: coverage ramps
//!   `1 → 0` across `[edge − aa, edge + aa]`. `aa = 0` ⇒ a hard (aliased) edge.

#[cfg(feature = "wgpu")]
use bytemuck::{Pod, Zeroable};

/// Shape discriminant carried in the instance so one quad pipeline draws every
/// SDF kind. Kept as a `u32` (not a Rust `enum`) so it is GPU-uploadable verbatim
/// and matches the `SHAPE_*` constants in `sdf.wgsl`.
pub mod shape {
    /// A filled, anti-aliased disc.
    pub const CIRCLE: u32 = 0;
    /// An annulus (filled ring with a hole) — `inner` is the hole radius.
    pub const RING: u32 = 1;
    /// A filled axis-aligned rounded square marker (`inner` = corner radius).
    pub const SQUARE: u32 = 2;
    /// A filled upward equilateral-ish triangle marker.
    pub const TRIANGLE: u32 = 3;
    /// A filled diamond (square rotated 45°).
    pub const DIAMOND: u32 = 4;
}

/// A screen-aligned quad instance carrying an SDF shape — the **one** instance
/// type the GPU pipeline draws (a `CircleInstance`/`RingInstance`/`MarkerInstance`
/// all lower into this). The vertex shader expands `center ± (radius + aa)` into a
/// quad; the fragment shader evaluates the SDF picked by `shape`.
///
/// 48 bytes, `#[repr(C)]` — the GPU instance-buffer layout (matches `sdf.wgsl`).
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "wgpu", derive(Pod, Zeroable))]
pub struct QuadInstance {
    /// Centre in screen pixels.
    pub center: [f32; 2],
    /// Outer radius (the disc / ring outer / marker half-extent), pixels.
    pub radius: f32,
    /// Inner radius — ring hole / rounded-square corner radius, pixels. `0` for a
    /// plain circle / sharp marker.
    pub inner: f32,
    /// Straight RGBA in `[0, 1]`.
    pub color: [f32; 4],
    /// Anti-alias band half-width in pixels.
    pub aa: f32,
    /// One of the [`shape`] tags.
    pub shape: u32,
    /// Padding to a 16-byte multiple (48 bytes) so an instance array is naturally
    /// aligned for the GPU.
    pub _pad: [f32; 2],
}

impl QuadInstance {
    /// The quad's half-extent: outer radius plus the AA band (the vertex shader
    /// and the CPU raster both inflate the bound by this so the AA fringe is not
    /// clipped).
    #[inline]
    pub fn half_extent(&self) -> f32 {
        self.radius + self.aa
    }
}

/// A filled anti-aliased disc. Lowers to a [`QuadInstance`] with `shape = CIRCLE`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CircleInstance {
    pub center: [f32; 2],
    pub radius: f32,
    pub color: [f32; 4],
    pub aa: f32,
}

impl CircleInstance {
    pub fn lower(self) -> QuadInstance {
        QuadInstance {
            center: self.center,
            radius: self.radius,
            inner: 0.0,
            color: self.color,
            aa: self.aa,
            shape: shape::CIRCLE,
            _pad: [0.0, 0.0],
        }
    }
}

/// An anti-aliased annulus (filled ring with a hole). `inner < radius`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RingInstance {
    pub center: [f32; 2],
    /// Outer radius.
    pub radius: f32,
    /// Inner (hole) radius — must be `< radius`.
    pub inner: f32,
    pub color: [f32; 4],
    pub aa: f32,
}

impl RingInstance {
    pub fn lower(self) -> QuadInstance {
        QuadInstance {
            center: self.center,
            radius: self.radius,
            inner: self.inner.clamp(0.0, self.radius),
            color: self.color,
            aa: self.aa,
            shape: shape::RING,
            _pad: [0.0, 0.0],
        }
    }
}

/// A filled marker glyph (square / triangle / diamond) sized by `radius`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MarkerInstance {
    pub center: [f32; 2],
    /// Half-extent of the marker (a square spans `2*radius`).
    pub radius: f32,
    /// Corner radius for [`shape::SQUARE`]; ignored by triangle/diamond.
    pub corner: f32,
    pub color: [f32; 4],
    pub aa: f32,
    /// One of [`shape::SQUARE`] / [`shape::TRIANGLE`] / [`shape::DIAMOND`].
    pub shape: u32,
}

impl MarkerInstance {
    pub fn lower(self) -> QuadInstance {
        QuadInstance {
            center: self.center,
            radius: self.radius,
            inner: self.corner,
            color: self.color,
            aa: self.aa,
            shape: self.shape,
            _pad: [0.0, 0.0],
        }
    }
}

/// A thick anti-aliased line segment (a "capsule": a stadium of half-width
/// `half_width` around the segment `a → b`). The GPU pipeline expands it into an
/// oriented quad covering the segment ± `(half_width + aa)`; the CPU raster does
/// the same with a per-pixel distance-to-segment.
///
/// 48 bytes, `#[repr(C)]` — the GPU line instance-buffer layout (matches
/// `line.wgsl`).
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "wgpu", derive(Pod, Zeroable))]
pub struct LineInstance {
    /// Segment start, screen pixels.
    pub a: [f32; 2],
    /// Segment end, screen pixels.
    pub b: [f32; 2],
    /// Half the stroke width (the segment's distance ceiling), pixels.
    pub half_width: f32,
    /// Anti-alias band half-width, pixels.
    pub aa: f32,
    /// `0` = butt cap (rectangle, no rounding past the endpoints); `1` = round cap
    /// (the full stadium). Matches `CAP_*` in `line.wgsl`.
    pub cap: u32,
    /// Padding to 16-byte alignment (the colour starts on a vec4 boundary).
    pub _pad0: u32,
    /// Straight RGBA in `[0, 1]`.
    pub color: [f32; 4],
}

/// Cap styles for [`LineInstance`].
pub mod cap {
    /// Square/butt cap — the line ends flush at the endpoints (no overshoot).
    pub const BUTT: u32 = 0;
    /// Round cap — a half-disc of `half_width` past each endpoint.
    pub const ROUND: u32 = 1;
}

impl LineInstance {
    /// A round-capped line between two points.
    pub fn round(a: [f32; 2], b: [f32; 2], half_width: f32, aa: f32, color: [f32; 4]) -> Self {
        Self { a, b, half_width, aa, cap: cap::ROUND, _pad0: 0, color }
    }
    /// A butt-capped line between two points.
    pub fn butt(a: [f32; 2], b: [f32; 2], half_width: f32, aa: f32, color: [f32; 4]) -> Self {
        Self { a, b, half_width, aa, cap: cap::BUTT, _pad0: 0, color }
    }
    /// Axis-aligned bounding box (inflated by `half_width + aa`) the raster needs
    /// to visit — `(min_x, min_y, max_x, max_y)`.
    #[inline]
    pub fn bounds(&self) -> (f32, f32, f32, f32) {
        let r = self.half_width + self.aa;
        let min_x = self.a[0].min(self.b[0]) - r;
        let max_x = self.a[0].max(self.b[0]) + r;
        let min_y = self.a[1].min(self.b[1]) - r;
        let max_y = self.a[1].max(self.b[1]) + r;
        (min_x, min_y, max_x, max_y)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// INJECT-ASSERT: each typed primitive lowers to a `QuadInstance` carrying the
    /// right shape tag + geometry (so one quad pipeline draws them all).
    #[test]
    fn typed_primitives_lower_to_the_right_quad_shape() {
        let c = CircleInstance { center: [10.0, 20.0], radius: 5.0, color: [1.0, 0.0, 0.0, 1.0], aa: 1.0 }
            .lower();
        assert_eq!(c.shape, shape::CIRCLE);
        assert_eq!(c.inner, 0.0);
        assert_eq!(c.center, [10.0, 20.0]);
        assert_eq!(c.half_extent(), 6.0);

        let r = RingInstance { center: [0.0, 0.0], radius: 8.0, inner: 4.0, color: [0.0; 4], aa: 1.5 }
            .lower();
        assert_eq!(r.shape, shape::RING);
        assert_eq!(r.inner, 4.0);

        // inner is clamped into [0, radius] so a malformed ring can't make the SDF
        // produce a negative-thickness annulus.
        let bad = RingInstance { center: [0.0, 0.0], radius: 3.0, inner: 9.0, color: [0.0; 4], aa: 1.0 }
            .lower();
        assert_eq!(bad.inner, 3.0, "inner clamped to radius");

        let m = MarkerInstance {
            center: [1.0, 2.0],
            radius: 6.0,
            corner: 1.0,
            color: [0.0; 4],
            aa: 1.0,
            shape: shape::DIAMOND,
        }
        .lower();
        assert_eq!(m.shape, shape::DIAMOND);
        assert_eq!(m.inner, 1.0, "corner carried in inner");
    }

    /// INJECT-ASSERT: a line's inflated bounds enclose both endpoints + the
    /// stroke + AA on every side.
    #[test]
    fn line_bounds_inflate_by_halfwidth_plus_aa() {
        let l = LineInstance::round([10.0, 10.0], [30.0, 14.0], 3.0, 1.0, [1.0; 4]);
        let (mnx, mny, mxx, mxy) = l.bounds();
        assert_eq!(mnx, 10.0 - 4.0);
        assert_eq!(mny, 10.0 - 4.0);
        assert_eq!(mxx, 30.0 + 4.0);
        assert_eq!(mxy, 14.0 + 4.0);
        assert_eq!(l.cap, cap::ROUND);
        assert_eq!(LineInstance::butt([0.0; 2], [1.0, 0.0], 1.0, 0.0, [0.0; 4]).cap, cap::BUTT);
    }
}