Skip to main content

smooth_frame/
rect.rs

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/// 矩形便捷封装。
10#[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    /// 创建矩形便捷封装。
20    ///
21    /// 宽高为非有限数、负数或零时会退化为单点闭合路径。
22    #[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    /// 设置矩形角半径。
33    ///
34    /// 负数或非有限数会按 0 处理,过大的半径会 clamp 到短边的一半。
35    #[must_use]
36    pub fn with_radius(mut self, radius: f64) -> Self {
37        self.radius = radius;
38        self
39    }
40
41    /// 设置 Sketch-like smoothing。
42    ///
43    /// 有限数会 clamp 到 `[0, 1]`,非有限数会按 0 处理。
44    #[must_use]
45    pub fn with_smoothing(mut self, smoothing: f64) -> Self {
46        self.smoothing = smoothing;
47        self
48    }
49
50    /// 生成矩形 smooth corner 路径。
51    #[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}