1use cranpose_ui_graphics::{ArcGeometry, CornerRadii, Point, Rect, StrokeCap, StrokeJoin};
11
12const INV_SQRT2: f32 = std::f32::consts::FRAC_1_SQRT_2;
13
14pub fn sdf_rounded_rect(p: Point, half_size: (f32, f32), radii: [f32; 4]) -> f32 {
19 let radius = match (p.x > 0.0, p.y > 0.0) {
20 (false, false) => radii[0],
21 (true, false) => radii[1],
22 (false, true) => radii[2],
23 (true, true) => radii[3],
24 };
25 let qx = p.x.abs() - half_size.0 + radius;
26 let qy = p.y.abs() - half_size.1 + radius;
27 let inside = qx.max(qy).min(0.0);
28 let outside = (qx.max(0.0).powi(2) + qy.max(0.0).powi(2)).sqrt();
29 inside + outside - radius
30}
31
32pub fn sdf_stroked_rounded_rect(
38 p: Point,
39 half_size: (f32, f32),
40 radii: [f32; 4],
41 half_width: f32,
42 join: StrokeJoin,
43) -> f32 {
44 let hw = half_width.max(0.0);
45 let geom = ((half_size.0 - hw).max(0.0), (half_size.1 - hw).max(0.0));
46
47 let mut outer_radii = [radii[0] + hw, radii[1] + hw, radii[2] + hw, radii[3] + hw];
48 if join != StrokeJoin::Round {
49 for (out, r) in outer_radii.iter_mut().zip(radii.iter()) {
52 if *r < 0.0001 {
53 *out = 0.0;
54 }
55 }
56 }
57 let inner_radii = [
58 (radii[0] - hw).max(0.0),
59 (radii[1] - hw).max(0.0),
60 (radii[2] - hw).max(0.0),
61 (radii[3] - hw).max(0.0),
62 ];
63
64 let outer = sdf_rounded_rect(p, (geom.0 + hw, geom.1 + hw), outer_radii);
65 let inner = sdf_rounded_rect(
66 p,
67 ((geom.0 - hw).max(0.0), (geom.1 - hw).max(0.0)),
68 inner_radii,
69 );
70 let mut dist = outer.max(-inner);
71
72 if join == StrokeJoin::Bevel {
73 let chamfer = (p.x.abs() + p.y.abs() - (geom.0 + geom.1 + hw)) * INV_SQRT2;
74 dist = dist.max(chamfer);
75 }
76 dist
77}
78
79pub fn sdf_arc_band(p: Point, arc: &ArcGeometry) -> f32 {
82 let ra = arc.mid_radius();
83 let rb = arc.half_thickness().max(0.0);
84 let sweep = arc.sweep_angle.clamp(0.0, cranpose_ui_graphics::TAU);
85 let half_sweep = sweep * 0.5;
86 let mid = arc.start_angle + half_sweep;
87
88 let (sm, cm) = mid.sin_cos();
89 let dx = p.x - arc.center.x;
90 let dy = p.y - arc.center.y;
91 let qx = (-sm * dx + cm * dy).abs();
93 let qy = cm * dx + sm * dy;
94
95 let sc = (half_sweep.sin().max(0.0), half_sweep.cos());
98
99 let mut dist = if sc.1 * qx > sc.0 * qy {
100 ((qx - sc.0 * ra).powi(2) + (qy - sc.1 * ra).powi(2)).sqrt() - rb
101 } else {
102 ((qx * qx + qy * qy).sqrt() - ra).abs() - rb
103 };
104
105 let plane = sc.1 * qx - sc.0 * qy;
107 match arc.cap {
108 StrokeCap::Butt => dist = dist.max(plane),
109 StrokeCap::Square => dist = dist.max(plane - rb),
110 StrokeCap::Round => {}
111 }
112 dist
113}
114
115pub fn coverage_for_distance(distance: f32) -> f32 {
118 if !distance.is_finite() {
119 return 0.0;
120 }
121 let t = ((distance + 0.5).clamp(0.0, 1.0)) as f64;
122 let smooth = t * t * (3.0 - 2.0 * t);
123 (1.0 - smooth) as f32
124}
125
126pub fn stroked_rect_coverage(
129 point: Point,
130 rect: Rect,
131 radii: Option<CornerRadii>,
132 half_width: f32,
133 join: StrokeJoin,
134) -> f32 {
135 let half_size = (rect.width * 0.5, rect.height * 0.5);
136 let local = Point::new(
137 point.x - (rect.x + half_size.0),
138 point.y - (rect.y + half_size.1),
139 );
140 let radii = radii.unwrap_or_default();
141 let distance = sdf_stroked_rounded_rect(
142 local,
143 half_size,
144 [
145 radii.top_left,
146 radii.top_right,
147 radii.bottom_left,
148 radii.bottom_right,
149 ],
150 half_width,
151 join,
152 );
153 coverage_for_distance(distance)
154}
155
156pub fn arc_coverage(point: Point, arc: &ArcGeometry) -> f32 {
158 coverage_for_distance(sdf_arc_band(point, arc))
159}
160
161#[cfg(test)]
162mod tests {
163 use std::f32::consts::{FRAC_PI_2, PI};
164
165 use cranpose_ui_graphics::TAU;
166
167 use super::*;
168
169 fn arc(inner: f32, outer: f32, start: f32, sweep: f32, cap: StrokeCap) -> ArcGeometry {
170 ArcGeometry::new(Point::ZERO, inner, outer, start, sweep, cap)
171 }
172
173 #[test]
174 fn rounded_rect_sdf_matches_known_distances() {
175 let d_center = sdf_rounded_rect(Point::ZERO, (10.0, 10.0), [0.0; 4]);
178 assert!((d_center + 10.0).abs() < 1e-4, "{d_center}");
179 let d_outside = sdf_rounded_rect(Point::new(15.0, 0.0), (10.0, 10.0), [0.0; 4]);
180 assert!((d_outside - 5.0).abs() < 1e-4, "{d_outside}");
181 }
182
183 #[test]
184 fn stroked_rect_covers_only_the_band_around_the_edge() {
185 let half = (12.0, 12.0);
187 let on_edge = sdf_stroked_rounded_rect(
188 Point::new(10.0, 0.0),
189 half,
190 [0.0; 4],
191 2.0,
192 StrokeJoin::Miter,
193 );
194 assert!(on_edge < 0.0, "the edge itself must be inside the stroke");
195 let inside =
196 sdf_stroked_rounded_rect(Point::new(4.0, 0.0), half, [0.0; 4], 2.0, StrokeJoin::Miter);
197 assert!(inside > 0.0, "the interior must be empty for a stroke");
198 let outside = sdf_stroked_rounded_rect(
199 Point::new(16.0, 0.0),
200 half,
201 [0.0; 4],
202 2.0,
203 StrokeJoin::Miter,
204 );
205 assert!(outside > 0.0, "well outside must be empty");
206 }
207
208 #[test]
209 fn miter_join_keeps_a_square_corner_round_join_does_not() {
210 let corner = Point::new(11.9, 11.9);
213 let miter =
214 sdf_stroked_rounded_rect(corner, (12.0, 12.0), [0.0; 4], 2.0, StrokeJoin::Miter);
215 let round =
216 sdf_stroked_rounded_rect(corner, (12.0, 12.0), [0.0; 4], 2.0, StrokeJoin::Round);
217 let bevel =
218 sdf_stroked_rounded_rect(corner, (12.0, 12.0), [0.0; 4], 2.0, StrokeJoin::Bevel);
219 assert!(miter < 0.0, "miter fills the corner point: {miter}");
220 assert!(round > 0.0, "round cuts the corner off: {round}");
221 assert!(bevel > 0.0, "bevel cuts the corner off: {bevel}");
222 assert!(
225 bevel > round,
226 "the bevel chord must cut deeper than the round arc: \
227 bevel={bevel} round={round}"
228 );
229 }
230
231 #[test]
232 fn full_ring_has_no_seam_at_the_wrap_point() {
233 let ring = arc(8.0, 12.0, 0.0, TAU, StrokeCap::Butt);
234 for step in 0..64 {
236 let angle = step as f32 / 64.0 * TAU;
237 let (sin, cos) = angle.sin_cos();
238 let p = Point::new(cos * 10.0, sin * 10.0);
239 let d = sdf_arc_band(p, &ring);
240 assert!(
241 d < 0.0,
242 "the ring centerline must be covered at angle {angle}: d={d}"
243 );
244 }
245 }
246
247 #[test]
248 fn butt_caps_cut_the_band_at_the_radial_ends() {
249 let band = arc(8.0, 12.0, 0.0, FRAC_PI_2, StrokeCap::Butt);
251 let inside = Point::new(10.0 * INV_SQRT2, 10.0 * INV_SQRT2);
253 assert!(sdf_arc_band(inside, &band) < 0.0);
254 let past_end = Point::new(-1.0, 10.0);
256 assert!(
257 sdf_arc_band(past_end, &band) > 0.0,
258 "butt cap must not bulge past the radial end"
259 );
260 let before_start = Point::new(10.0, -1.0);
262 assert!(sdf_arc_band(before_start, &band) > 0.0);
263 }
264
265 #[test]
266 fn round_caps_bulge_past_the_radial_ends_and_square_caps_project() {
267 let round = arc(8.0, 12.0, 0.0, FRAC_PI_2, StrokeCap::Round);
268 let square = arc(8.0, 12.0, 0.0, FRAC_PI_2, StrokeCap::Square);
269 let before_start = Point::new(10.0, -1.0);
271 assert!(
272 sdf_arc_band(before_start, &round) < 0.0,
273 "round cap must cover the semicircle past the end"
274 );
275 assert!(
276 sdf_arc_band(before_start, &square) < 0.0,
277 "square cap must cover the projection past the end"
278 );
279 let far = Point::new(10.0, -3.0);
281 assert!(sdf_arc_band(far, &round) > 0.0);
282 assert!(sdf_arc_band(far, &square) > 0.0);
283 }
284
285 #[test]
286 fn annular_sector_has_flat_radial_edges() {
287 let sector = arc(6.0, 12.0, 0.0, PI, StrokeCap::Butt);
291 for radius in [6.5, 8.0, 10.0, 11.5] {
292 let p = Point::new(radius * (0.01f32).cos(), radius * (0.01f32).sin());
294 assert!(
295 sdf_arc_band(p, §or) < 0.0,
296 "radius {radius} just inside the sweep must be covered"
297 );
298 let q = Point::new(radius * (-0.2f32).cos(), radius * (-0.2f32).sin());
300 assert!(
301 sdf_arc_band(q, §or) > 0.0,
302 "radius {radius} just outside the sweep must be empty"
303 );
304 }
305 }
306
307 #[test]
308 fn wedge_with_zero_inner_radius_reaches_the_center() {
309 let wedge = arc(0.0, 10.0, 0.0, FRAC_PI_2, StrokeCap::Butt);
310 assert!(sdf_arc_band(Point::new(0.5, 0.5), &wedge) < 0.0);
311 assert!(sdf_arc_band(Point::new(-0.5, -0.5), &wedge) > 0.0);
312 }
313
314 #[test]
315 fn degenerate_arcs_never_produce_nan_coverage() {
316 for geometry in [
317 arc(0.0, 0.0, 0.0, 0.0, StrokeCap::Butt),
318 arc(5.0, 5.0, 0.0, 1.0, StrokeCap::Round),
319 arc(0.0, 10.0, 0.0, 0.0, StrokeCap::Square),
320 ArcGeometry::new(Point::ZERO, f32::NAN, 1.0, 0.0, 1.0, StrokeCap::Butt),
321 ] {
322 for p in [Point::ZERO, Point::new(3.0, -4.0), Point::new(-9.0, 9.0)] {
323 let value = arc_coverage(p, &geometry);
324 assert!(value.is_finite(), "coverage must stay finite: {value}");
325 assert!((0.0..=1.0).contains(&value), "{value}");
326 }
327 }
328 }
329
330 #[test]
331 fn coverage_saturates_and_antialiases() {
332 assert_eq!(coverage_for_distance(-5.0), 1.0);
333 assert_eq!(coverage_for_distance(5.0), 0.0);
334 assert!((coverage_for_distance(0.0) - 0.5).abs() < 1e-5);
335 assert_eq!(coverage_for_distance(f32::NAN), 0.0);
336 }
337
338 #[test]
339 fn stroked_rect_coverage_uses_the_inflated_bounds() {
340 let bounds = Rect {
342 x: 8.0,
343 y: 8.0,
344 width: 24.0,
345 height: 24.0,
346 };
347 let on_edge =
348 stroked_rect_coverage(Point::new(10.0, 20.0), bounds, None, 2.0, StrokeJoin::Miter);
349 assert!(on_edge > 0.9, "the stroked edge must be opaque: {on_edge}");
350 let interior =
351 stroked_rect_coverage(Point::new(20.0, 20.0), bounds, None, 2.0, StrokeJoin::Miter);
352 assert_eq!(interior, 0.0, "a stroke must not fill its interior");
353 }
354}