use crate::render::prim::{shape, LineInstance, QuadInstance};
#[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)
}
#[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)
}
#[inline]
pub fn circle_coverage(dist: f32, radius: f32, aa: f32) -> f32 {
coverage_from_sd(dist - radius, aa)
}
#[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)
}
#[inline]
pub fn square_coverage(px: f32, py: f32, half: f32, corner: f32, aa: f32) -> f32 {
let corner = corner.clamp(0.0, half);
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); let sd = outside + inside - corner;
coverage_from_sd(sd, aa)
}
#[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)
}
#[inline]
pub fn triangle_coverage(px: f32, py: f32, half: f32, aa: f32) -> f32 {
let bottom = py - half;
let inv = 1.0 / (5.0f32).sqrt(); let right = (px * 2.0 - (py + half)) * inv;
let left = (px * -2.0 - (py + half)) * inv;
let sd = bottom.max(right).max(left);
coverage_from_sd(sd, aa)
}
#[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,
}
}
#[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)
}
#[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 {
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; 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() {
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;
}
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;
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");
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() {
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]);
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");
assert!(line_coverage(&butt, [50.0, 50.0]) > 0.99);
assert!(line_coverage(&round, [50.0, 50.0]) > 0.99);
}
}