1use cranpose_ui_graphics::{ArcGeometry, CornerRadii, Point, Rect, StrokeCap, StrokeJoin};
14
15mod edge_coverage;
16
17const INV_SQRT2: f32 = std::f32::consts::FRAC_1_SQRT_2;
18
19pub fn sdf_rounded_rect(p: Point, half_size: (f32, f32), radii: [f32; 4]) -> f32 {
24 let reach = RoundedReach::at(p, half_size, radii);
25 reach.inside + reach.outside_squared.sqrt() - reach.radius
26}
27
28struct RoundedReach {
32 inside: f32,
33 outside_squared: f32,
34 radius: f32,
35}
36
37impl RoundedReach {
38 fn at(p: Point, half_size: (f32, f32), radii: [f32; 4]) -> Self {
39 let radius = match (p.x > 0.0, p.y > 0.0) {
40 (false, false) => radii[0],
41 (true, false) => radii[1],
42 (false, true) => radii[2],
43 (true, true) => radii[3],
44 };
45 let qx = p.x.abs() - half_size.0 + radius;
46 let qy = p.y.abs() - half_size.1 + radius;
47 Self {
48 inside: qx.max(qy).min(0.0),
49 outside_squared: qx.max(0.0).powi(2) + qy.max(0.0).powi(2),
50 radius,
51 }
52 }
53}
54
55pub fn sdf_stroked_rounded_rect(
61 p: Point,
62 half_size: (f32, f32),
63 radii: [f32; 4],
64 half_width: f32,
65 join: StrokeJoin,
66) -> f32 {
67 StrokedBox::new(Point::ZERO, half_size, radii, half_width, join).distance(p)
68}
69
70pub struct StrokedBox {
73 center: Point,
74 outer: ((f32, f32), [f32; 4]),
75 inner: ((f32, f32), [f32; 4]),
76 bevel: Option<f32>,
78}
79
80impl StrokedBox {
81 pub fn new(
84 center: Point,
85 half_size: (f32, f32),
86 radii: [f32; 4],
87 half_width: f32,
88 join: StrokeJoin,
89 ) -> Self {
90 let hw = half_width.max(0.0);
91 let geom = ((half_size.0 - hw).max(0.0), (half_size.1 - hw).max(0.0));
92
93 let mut outer_radii = [radii[0] + hw, radii[1] + hw, radii[2] + hw, radii[3] + hw];
94 if join != StrokeJoin::Round {
95 for (out, r) in outer_radii.iter_mut().zip(radii.iter()) {
96 if *r < 0.0001 {
97 *out = 0.0;
98 }
99 }
100 }
101 let inner_radii = [
102 (radii[0] - hw).max(0.0),
103 (radii[1] - hw).max(0.0),
104 (radii[2] - hw).max(0.0),
105 (radii[3] - hw).max(0.0),
106 ];
107 Self {
108 center,
109 outer: ((geom.0 + hw, geom.1 + hw), outer_radii),
110 inner: (
111 ((geom.0 - hw).max(0.0), (geom.1 - hw).max(0.0)),
112 inner_radii,
113 ),
114 bevel: (join == StrokeJoin::Bevel).then_some(geom.0 + geom.1 + hw),
115 }
116 }
117
118 pub fn of_rect(
120 rect: Rect,
121 radii: Option<CornerRadii>,
122 half_width: f32,
123 join: StrokeJoin,
124 ) -> Self {
125 let half_size = (rect.width * 0.5, rect.height * 0.5);
126 let radii = radii.unwrap_or_default();
127 Self::new(
128 Point::new(rect.x + half_size.0, rect.y + half_size.1),
129 half_size,
130 [
131 radii.top_left,
132 radii.top_right,
133 radii.bottom_left,
134 radii.bottom_right,
135 ],
136 half_width,
137 join,
138 )
139 }
140
141 pub fn distance(&self, point: Point) -> f32 {
143 let p = Point::new(point.x - self.center.x, point.y - self.center.y);
144 let outer = RoundedReach::at(p, self.outer.0, self.outer.1);
145 let inner = RoundedReach::at(p, self.inner.0, self.inner.1);
146 let outer_outside = outer.outside_squared.sqrt();
147 let inner_outside = if inner.outside_squared.to_bits() == outer.outside_squared.to_bits() {
151 outer_outside
152 } else {
153 inner.outside_squared.sqrt()
154 };
155 let mut dist = (outer.inside + outer_outside - outer.radius)
156 .max(-(inner.inside + inner_outside - inner.radius));
157 if let Some(cut) = self.bevel {
158 let chamfer = (p.x.abs() + p.y.abs() - cut) * INV_SQRT2;
159 dist = dist.max(chamfer);
160 }
161 dist
162 }
163}
164
165pub fn sdf_arc_band(p: Point, arc: &ArcGeometry) -> f32 {
168 ArcBand::new(arc).distance(p)
169}
170
171pub struct ArcBand {
176 center: Point,
177 mid_radius: f32,
178 half_thickness: f32,
179 start: Point,
182 end: Point,
183 span: Span,
184 cap: StrokeCap,
185}
186
187#[derive(Clone, Copy)]
189enum Span {
190 Narrow,
193 Wide,
195 Full,
197}
198
199impl ArcBand {
200 pub fn new(arc: &ArcGeometry) -> Self {
202 let sweep = arc.sweep_angle.clamp(0.0, cranpose_ui_graphics::TAU);
203 let direction = |angle: f32| {
204 let (sin, cos) = angle.sin_cos();
205 Point::new(cos, sin)
206 };
207 Self {
208 center: arc.center,
209 mid_radius: arc.mid_radius(),
210 half_thickness: arc.half_thickness().max(0.0),
211 start: direction(arc.start_angle),
212 end: direction(arc.start_angle + sweep),
213 span: if sweep >= cranpose_ui_graphics::TAU {
214 Span::Full
215 } else if sweep > std::f32::consts::PI {
216 Span::Wide
217 } else {
218 Span::Narrow
219 },
220 cap: arc.cap,
221 }
222 }
223
224 pub fn distance(&self, p: Point) -> f32 {
226 let ra = self.mid_radius;
227 let rb = self.half_thickness;
228 let dx = p.x - self.center.x;
229 let dy = p.y - self.center.y;
230 let past_start = self.start.y * dx - self.start.x * dy;
233 let past_end = self.end.x * dy - self.end.y * dx;
234 let inside = match self.span {
235 Span::Narrow => past_start <= 0.0 && past_end <= 0.0,
236 Span::Wide => past_start <= 0.0 || past_end <= 0.0,
237 Span::Full => true,
238 };
239 if inside {
240 let band = ((dx * dx + dy * dy).sqrt() - ra).abs() - rb;
241 return match self.cap {
242 StrokeCap::Butt => band
243 .max(butt_cut(self.start, past_start, dx, dy))
244 .max(butt_cut(self.end, past_end, dx, dy)),
245 StrokeCap::Round | StrokeCap::Square => band,
246 };
247 }
248 let cap = |end: Point, past: f32| {
250 let round = ((dx - ra * end.x).powi(2) + (dy - ra * end.y).powi(2)).sqrt() - rb;
251 match self.cap {
252 StrokeCap::Butt => round.max(past),
253 StrokeCap::Square => round.max(past - rb),
254 StrokeCap::Round => round,
255 }
256 };
257 match (self.cap, past_start > 0.0, past_end > 0.0) {
258 (StrokeCap::Butt, true, false) => cap(self.start, past_start),
262 (StrokeCap::Butt, false, true) => cap(self.end, past_end),
263 _ => cap(self.start, past_start).min(cap(self.end, past_end)),
264 }
265 }
266
267 pub fn coverage(&self, p: Point) -> f32 {
269 coverage_for_distance(self.distance(p))
270 }
271}
272
273fn butt_cut(end: Point, past: f32, dx: f32, dy: f32) -> f32 {
277 if past <= 0.0 && end.x * dx + end.y * dy > 0.0 {
278 past
279 } else {
280 f32::NEG_INFINITY
281 }
282}
283
284pub fn coverage_for_distance(distance: f32) -> f32 {
287 if !distance.is_finite() {
288 return 0.0;
289 }
290 let t = (distance + 0.5).clamp(0.0, 1.0);
291 if t <= 0.0 {
293 return 1.0;
294 }
295 if t >= 1.0 {
296 return 0.0;
297 }
298 edge_coverage::edge_coverage(t)
299}
300
301pub fn stroked_rect_coverage(
304 point: Point,
305 rect: Rect,
306 radii: Option<CornerRadii>,
307 half_width: f32,
308 join: StrokeJoin,
309) -> f32 {
310 coverage_for_distance(StrokedBox::of_rect(rect, radii, half_width, join).distance(point))
311}
312
313pub fn arc_coverage(point: Point, arc: &ArcGeometry) -> f32 {
315 ArcBand::new(arc).coverage(point)
316}
317
318#[cfg(test)]
319#[path = "tests/shape_sdf_tests.rs"]
320mod tests;