1use crate::render::prim::{shape, LineInstance, QuadInstance};
17
18#[inline]
21pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
22 if edge0 == edge1 {
23 return if x < edge0 { 0.0 } else { 1.0 };
24 }
25 let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
26 t * t * (3.0 - 2.0 * t)
27}
28
29#[inline]
34pub fn coverage_from_sd(d: f32, aa: f32) -> f32 {
35 if aa <= 0.0 {
36 return if d <= 0.0 { 1.0 } else { 0.0 };
37 }
38 1.0 - smoothstep(-aa, aa, d)
39}
40
41#[inline]
44pub fn circle_coverage(dist: f32, radius: f32, aa: f32) -> f32 {
45 coverage_from_sd(dist - radius, aa)
46}
47
48#[inline]
53pub fn ring_coverage(dist: f32, outer: f32, inner: f32, aa: f32) -> f32 {
54 let sd = (dist - outer).max(inner - dist);
55 coverage_from_sd(sd, aa)
56}
57
58#[inline]
62pub fn square_coverage(px: f32, py: f32, half: f32, corner: f32, aa: f32) -> f32 {
63 let corner = corner.clamp(0.0, half);
64 let dx = px.abs() - (half - corner);
66 let dy = py.abs() - (half - corner);
67 let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
68 let inside = dx.max(dy).min(0.0); let sd = outside + inside - corner;
70 coverage_from_sd(sd, aa)
71}
72
73#[inline]
76pub fn diamond_coverage(px: f32, py: f32, half: f32, aa: f32) -> f32 {
77 let sd = (px.abs() + py.abs()) - half;
78 coverage_from_sd(sd, aa)
79}
80
81#[inline]
86pub fn triangle_coverage(px: f32, py: f32, half: f32, aa: f32) -> f32 {
87 let bottom = py - half;
89 let inv = 1.0 / (5.0f32).sqrt(); let right = (px * 2.0 - (py + half)) * inv;
93 let left = (px * -2.0 - (py + half)) * inv;
95 let sd = bottom.max(right).max(left);
96 coverage_from_sd(sd, aa)
97}
98
99#[inline]
103pub fn quad_coverage(inst: &QuadInstance, dx: f32, dy: f32) -> f32 {
104 match inst.shape {
105 shape::CIRCLE => circle_coverage((dx * dx + dy * dy).sqrt(), inst.radius, inst.aa),
106 shape::RING => ring_coverage((dx * dx + dy * dy).sqrt(), inst.radius, inst.inner, inst.aa),
107 shape::SQUARE => square_coverage(dx, dy, inst.radius, inst.inner, inst.aa),
108 shape::TRIANGLE => triangle_coverage(dx, dy, inst.radius, inst.aa),
109 shape::DIAMOND => diamond_coverage(dx, dy, inst.radius, inst.aa),
110 _ => 0.0,
111 }
112}
113
114#[inline]
118pub fn point_segment(p: [f32; 2], a: [f32; 2], b: [f32; 2]) -> (f32, f32) {
119 let abx = b[0] - a[0];
120 let aby = b[1] - a[1];
121 let apx = p[0] - a[0];
122 let apy = p[1] - a[1];
123 let len2 = abx * abx + aby * aby;
124 let t = if len2 <= 1e-12 { 0.0 } else { ((apx * abx + apy * aby) / len2).clamp(0.0, 1.0) };
125 let cx = a[0] + abx * t;
126 let cy = a[1] + aby * t;
127 let dx = p[0] - cx;
128 let dy = p[1] - cy;
129 (dx * dx + dy * dy, t)
130}
131
132#[inline]
137pub fn line_coverage(inst: &LineInstance, p: [f32; 2]) -> f32 {
138 let (d2, _t) = point_segment(p, inst.a, inst.b);
139 let dist = d2.sqrt();
140 let mut cov = coverage_from_sd(dist - inst.half_width, inst.aa);
141
142 if inst.cap == crate::render::prim::cap::BUTT {
143 let abx = inst.b[0] - inst.a[0];
147 let aby = inst.b[1] - inst.a[1];
148 let len = (abx * abx + aby * aby).sqrt();
149 if len > 1e-6 {
150 let ux = abx / len;
151 let uy = aby / len;
152 let s = (p[0] - inst.a[0]) * ux + (p[1] - inst.a[1]) * uy; let axis_sd = (-s).max(s - len);
155 cov *= coverage_from_sd(axis_sd, inst.aa);
156 }
157 }
158 cov
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::render::prim::{CircleInstance, LineInstance, MarkerInstance, RingInstance};
165
166 #[test]
167 fn coverage_kernel_is_monotone_across_the_band() {
168 assert_eq!(coverage_from_sd(-5.0, 1.0), 1.0);
170 assert_eq!(coverage_from_sd(5.0, 1.0), 0.0);
171 let mut prev = 1.0;
172 let mut d = -1.0;
173 while d <= 1.0 {
174 let c = coverage_from_sd(d, 1.0);
175 assert!(c <= prev + 1e-6, "monotone non-increasing as d grows");
176 prev = c;
177 d += 0.1;
178 }
179 assert_eq!(coverage_from_sd(-0.01, 0.0), 1.0);
181 assert_eq!(coverage_from_sd(0.01, 0.0), 0.0);
182 }
183
184 #[test]
185 fn circle_lit_only_inside_radius_band() {
186 let r = 10.0;
187 let aa = 1.0;
188 assert!(circle_coverage(0.0, r, aa) > 0.99, "centre fully lit");
189 assert!(circle_coverage(r - aa - 0.5, r, aa) > 0.99, "just inside fully lit");
190 assert!(circle_coverage(r + aa + 0.5, r, aa) < 0.01, "outside band dark");
191 assert!((circle_coverage(r, r, aa) - 0.5).abs() < 0.05, "edge ≈ half coverage");
192 }
193
194 #[test]
195 fn ring_has_a_hole() {
196 let outer = 12.0;
197 let inner = 6.0;
198 let aa = 1.0;
199 assert!(ring_coverage(9.0, outer, inner, aa) > 0.99, "in the band is lit");
200 assert!(ring_coverage(0.0, outer, inner, aa) < 0.01, "centre is a hole");
201 assert!(ring_coverage(20.0, outer, inner, aa) < 0.01, "outside dark");
202 }
203
204 #[test]
205 fn marker_shapes_differ_at_a_corner() {
206 let half = 10.0;
207 let aa = 0.5;
208 assert!(square_coverage(8.0, 8.0, half, 0.0, aa) > 0.9, "square fills its corner");
211 assert!(diamond_coverage(8.0, 8.0, half, aa) < 0.1, "diamond excludes the corner");
212 assert!(triangle_coverage(0.0, 7.0, half, aa) > 0.9, "triangle base-centre inside");
215 assert!(triangle_coverage(9.0, -9.0, half, aa) < 0.1, "triangle top-corner outside");
216 }
217
218 #[test]
219 fn quad_coverage_dispatches_per_shape() {
220 let c = CircleInstance { center: [0.0, 0.0], radius: 5.0, color: [1.0; 4], aa: 1.0 }.lower();
221 assert!(quad_coverage(&c, 0.0, 0.0) > 0.99);
222 assert!(quad_coverage(&c, 10.0, 0.0) < 0.01);
223
224 let ring = RingInstance { center: [0.0; 2], radius: 8.0, inner: 4.0, color: [1.0; 4], aa: 1.0 }
225 .lower();
226 assert!(quad_coverage(&ring, 0.0, 0.0) < 0.01, "ring centre hole");
227 assert!(quad_coverage(&ring, 6.0, 0.0) > 0.9, "ring band lit");
228
229 let diamond = MarkerInstance {
230 center: [0.0; 2],
231 radius: 10.0,
232 corner: 0.0,
233 color: [1.0; 4],
234 aa: 0.5,
235 shape: shape::DIAMOND,
236 }
237 .lower();
238 assert!(quad_coverage(&diamond, 0.0, 0.0) > 0.9);
239 assert!(quad_coverage(&diamond, 9.0, 9.0) < 0.1);
240 }
241
242 #[test]
243 fn line_coverage_matches_requested_width() {
244 let l = LineInstance::round([10.0, 50.0], [90.0, 50.0], 3.0, 1.0, [1.0; 4]);
246 assert!(line_coverage(&l, [50.0, 50.0]) > 0.99, "on axis lit");
247 assert!(line_coverage(&l, [50.0, 52.0]) > 0.9, "within half_width lit");
248 assert!(line_coverage(&l, [50.0, 56.0]) < 0.05, "beyond width+aa dark");
249 assert!((line_coverage(&l, [50.0, 53.0]) - 0.5).abs() < 0.1, "edge ≈ half coverage");
250 }
251
252 #[test]
253 fn butt_cap_ends_flush_round_cap_overshoots() {
254 let butt = LineInstance::butt([20.0, 50.0], [80.0, 50.0], 4.0, 1.0, [1.0; 4]);
255 let round = LineInstance::round([20.0, 50.0], [80.0, 50.0], 4.0, 1.0, [1.0; 4]);
256 assert!(round.cap == crate::render::prim::cap::ROUND);
258 assert!(line_coverage(&round, [82.0, 50.0]) > 0.5, "round cap overshoots");
259 assert!(line_coverage(&butt, [82.0, 50.0]) < 0.2, "butt cap ends flush");
260 assert!(line_coverage(&butt, [50.0, 50.0]) > 0.99);
262 assert!(line_coverage(&round, [50.0, 50.0]) > 0.99);
263 }
264}