1use std::f64::consts::PI;
2
3use crate::geometry::{Point, Vector};
4use crate::math::{clamp01, EPSILON};
5use crate::path::{CubicSegment, SmoothPath};
6
7const SKETCH_FALLBACK_EFFECTIVE_SMOOTHING: f64 = 0.005;
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct SmoothRect {
12 width: f64,
13 height: f64,
14 radius: f64,
15 smoothing: f64,
16}
17
18impl SmoothRect {
19 #[must_use]
23 pub fn new(width: f64, height: f64) -> Self {
24 Self {
25 width,
26 height,
27 radius: 0.0,
28 smoothing: 0.0,
29 }
30 }
31
32 #[must_use]
36 pub fn with_radius(mut self, radius: f64) -> Self {
37 self.radius = radius;
38 self
39 }
40
41 #[must_use]
45 pub fn with_smoothing(mut self, smoothing: f64) -> Self {
46 self.smoothing = smoothing;
47 self
48 }
49
50 #[must_use]
52 pub fn to_path(&self) -> SmoothPath {
53 let width = sanitize_dimension(self.width);
54 let height = sanitize_dimension(self.height);
55 let requested_radius = sanitize_dimension(self.radius);
56 let smoothing = if self.smoothing.is_finite() {
57 clamp01(self.smoothing)
58 } else {
59 0.0
60 };
61
62 if width <= EPSILON || height <= EPSILON {
63 let mut path = SmoothPath::new();
64 path.move_to(Point::new(0.0, 0.0));
65 path.close();
66 return path;
67 }
68
69 let radius = requested_radius.min(width.min(height) / 2.0);
70 if radius <= EPSILON {
71 let mut path = SmoothPath::new();
72 path.move_to(Point::new(0.0, 0.0));
73 path.line_to(Point::new(width, 0.0));
74 path.line_to(Point::new(width, height));
75 path.line_to(Point::new(0.0, height));
76 path.close();
77 return path;
78 }
79
80 let horizontal = sketch_rect_axis(radius, smoothing, width);
81 let vertical = sketch_rect_axis(radius, smoothing, height);
82 let corners = [
83 RectCorner::new(
84 Point::new(0.0, 0.0),
85 Vector::new(0.0, 1.0),
86 vertical,
87 Vector::new(1.0, 0.0),
88 horizontal,
89 ),
90 RectCorner::new(
91 Point::new(width, 0.0),
92 Vector::new(-1.0, 0.0),
93 horizontal,
94 Vector::new(0.0, 1.0),
95 vertical,
96 ),
97 RectCorner::new(
98 Point::new(width, height),
99 Vector::new(0.0, -1.0),
100 vertical,
101 Vector::new(-1.0, 0.0),
102 horizontal,
103 ),
104 RectCorner::new(
105 Point::new(0.0, height),
106 Vector::new(1.0, 0.0),
107 horizontal,
108 Vector::new(0.0, -1.0),
109 vertical,
110 ),
111 ];
112
113 let cubics = corners
114 .iter()
115 .map(|corner| rect_corner_cubics(*corner, radius))
116 .collect::<Vec<_>>();
117
118 let mut path = SmoothPath::new();
119 let mut current = corners[0].end();
120 path.move_to(current);
121
122 for index in 1..corners.len() {
123 push_line_if_needed(&mut path, &mut current, corners[index].start());
124 push_cubics_if_needed(&mut path, &mut current, &cubics[index]);
125 }
126 push_line_if_needed(&mut path, &mut current, corners[0].start());
127 push_cubics_if_needed(&mut path, &mut current, &cubics[0]);
128 path.close();
129 path
130 }
131}
132
133#[derive(Debug, Clone, Copy)]
134struct RectAxis {
135 influence: f64,
136 alpha: f64,
137}
138
139#[derive(Debug, Clone, Copy)]
140struct RectCorner {
141 origin: Point,
142 incoming_axis: Vector,
143 incoming: RectAxis,
144 outgoing_axis: Vector,
145 outgoing: RectAxis,
146}
147
148impl RectCorner {
149 fn new(
150 origin: Point,
151 incoming_axis: Vector,
152 incoming: RectAxis,
153 outgoing_axis: Vector,
154 outgoing: RectAxis,
155 ) -> Self {
156 Self {
157 origin,
158 incoming_axis,
159 incoming,
160 outgoing_axis,
161 outgoing,
162 }
163 }
164
165 fn start(self) -> Point {
166 self.origin + self.incoming_axis * self.incoming.influence
167 }
168
169 fn end(self) -> Point {
170 self.origin + self.outgoing_axis * self.outgoing.influence
171 }
172}
173
174fn sketch_rect_axis(radius: f64, smoothing: f64, side: f64) -> RectAxis {
175 let raw_influence = (1.0 + smoothing) * radius;
176 let limit = side / 2.0;
177 let saturated_influence = raw_influence.min(limit);
178 let effective_smoothing = clamp01(saturated_influence / radius - 1.0);
179
180 if raw_influence >= limit && effective_smoothing < SKETCH_FALLBACK_EFFECTIVE_SMOOTHING {
181 return RectAxis {
182 influence: radius.min(limit),
183 alpha: 0.0,
184 };
185 }
186
187 RectAxis {
188 influence: saturated_influence,
189 alpha: effective_smoothing * PI / 4.0,
190 }
191}
192
193fn rect_corner_cubics(corner: RectCorner, radius: f64) -> [CubicSegment; 3] {
194 let alpha0 = corner.incoming.alpha;
195 let alpha1 = corner.outgoing.alpha;
196 let influence0 = corner.incoming.influence;
197 let influence1 = corner.outgoing.influence;
198
199 let tangent0 = radius - radius * (alpha0 / 2.0).tan();
200 let handle0 = (influence0 - tangent0) / 3.0;
201 let tangent1 = radius - radius * (alpha1 / 2.0).tan();
202 let handle1 = (influence1 - tangent1) / 3.0;
203
204 let p1 = corner.origin
205 + corner.incoming_axis * (radius - radius * alpha0.sin())
206 + corner.outgoing_axis * (radius - radius * alpha0.cos());
207 let p2 = corner.origin
208 + corner.incoming_axis * (radius - radius * alpha1.cos())
209 + corner.outgoing_axis * (radius - radius * alpha1.sin());
210
211 let middle_arc_angle = (PI / 2.0 - alpha0 - alpha1).max(0.0);
212 let arc_handle = if middle_arc_angle <= EPSILON {
213 0.0
214 } else {
215 (4.0 / 3.0) * (middle_arc_angle / 4.0).tan() * radius
216 };
217
218 [
219 CubicSegment {
220 from: corner.start(),
221 ctrl1: corner.origin + corner.incoming_axis * (influence0 - 2.0 * handle0),
222 ctrl2: corner.origin + corner.incoming_axis * tangent0,
223 to: p1,
224 },
225 CubicSegment {
226 from: p1,
227 ctrl1: p1
228 + corner.incoming_axis * (-arc_handle * alpha0.cos())
229 + corner.outgoing_axis * (arc_handle * alpha0.sin()),
230 ctrl2: p2
231 + corner.incoming_axis * (arc_handle * alpha1.sin())
232 + corner.outgoing_axis * (-arc_handle * alpha1.cos()),
233 to: p2,
234 },
235 CubicSegment {
236 from: p2,
237 ctrl1: corner.origin + corner.outgoing_axis * tangent1,
238 ctrl2: corner.origin + corner.outgoing_axis * (tangent1 + handle1),
239 to: corner.end(),
240 },
241 ]
242}
243
244fn push_line_if_needed(path: &mut SmoothPath, current: &mut Point, to: Point) {
245 if !points_close(*current, to, EPSILON) {
246 path.line_to(to);
247 }
248 *current = to;
249}
250
251fn push_cubics_if_needed(path: &mut SmoothPath, current: &mut Point, cubics: &[CubicSegment]) {
252 for (index, cubic) in cubics.iter().enumerate() {
253 if index == 1 || !cubic_is_zero(*cubic) {
254 path.cubic_to(cubic.ctrl1, cubic.ctrl2, cubic.to);
255 }
256 *current = cubic.to;
257 }
258}
259
260fn cubic_is_zero(cubic: CubicSegment) -> bool {
261 points_close(cubic.from, cubic.ctrl1, EPSILON)
262 && points_close(cubic.from, cubic.ctrl2, EPSILON)
263 && points_close(cubic.from, cubic.to, EPSILON)
264}
265
266fn points_close(a: Point, b: Point, tolerance: f64) -> bool {
267 (a.x - b.x).abs() <= tolerance && (a.y - b.y).abs() <= tolerance
268}
269
270fn sanitize_dimension(value: f64) -> f64 {
271 if value.is_finite() && value > 0.0 {
272 value
273 } else {
274 0.0
275 }
276}