Skip to main content

smooth_frame/
corner.rs

1use std::f64::consts::PI;
2
3use crate::error::SmoothError;
4use crate::geometry::{Point, Vector};
5use crate::math::{clamp01, EPSILON};
6use crate::path::CubicSegment;
7
8/// 单个 smooth corner 解析后的参数,便于测试或调试 Sketch 对齐。
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct SmoothCornerGeometry {
11    /// 原始角点。
12    pub origin: Point,
13    /// 从角点指向上一条边的单位方向。
14    pub incoming_axis: Vector,
15    /// 从角点指向下一条边的单位方向。
16    pub outgoing_axis: Vector,
17    /// clamp 后实际使用的核心圆半径。
18    pub radius: f64,
19    /// clamp 到 `[0, 1]` 后的平滑系数。
20    pub smoothing: f64,
21    /// incoming 方向的最大影响范围。
22    pub incoming_limit: f64,
23    /// outgoing 方向的最大影响范围。
24    pub outgoing_limit: f64,
25    /// incoming 与 outgoing 方向之间的夹角,单位为弧度。
26    pub angle: f64,
27    /// 核心圆在未平滑时的切点距离。
28    pub base_tangent: f64,
29    /// incoming 方向最终影响范围。
30    pub incoming_influence: f64,
31    /// outgoing 方向最终影响范围。
32    pub outgoing_influence: f64,
33    /// incoming 侧过渡角,单位为弧度。
34    pub alpha0: f64,
35    /// outgoing 侧过渡角,单位为弧度。
36    pub alpha1: f64,
37    /// 中间圆弧段角度,单位为弧度。
38    pub middle_arc_angle: f64,
39    /// 角点 smooth cubic 的起点。
40    pub start: Point,
41    /// 角点 smooth cubic 的终点。
42    pub end: Point,
43}
44
45/// 任意凸角的 Sketch-like smooth corner。
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct SmoothCorner {
48    origin: Point,
49    incoming_axis: Vector,
50    outgoing_axis: Vector,
51    radius: f64,
52    smoothing: f64,
53    incoming_limit: f64,
54    outgoing_limit: f64,
55}
56
57impl SmoothCorner {
58    /// 创建一个任意凸角的 smooth corner。
59    ///
60    /// `incoming_axis` 从角点指向上一条边,`outgoing_axis` 从角点指向下一条边。
61    #[must_use]
62    pub fn new(origin: Point, incoming_axis: Vector, outgoing_axis: Vector) -> Self {
63        Self {
64            origin,
65            incoming_axis,
66            outgoing_axis,
67            radius: 0.0,
68            smoothing: 0.0,
69            incoming_limit: f64::INFINITY,
70            outgoing_limit: f64::INFINITY,
71        }
72    }
73
74    /// 设置核心圆半径。
75    #[must_use]
76    pub fn with_radius(mut self, radius: f64) -> Self {
77        self.radius = radius;
78        self
79    }
80
81    /// 设置 Sketch-like smoothing,计算时会 clamp 到 `[0, 1]`。
82    #[must_use]
83    pub fn with_smoothing(mut self, smoothing: f64) -> Self {
84        self.smoothing = smoothing;
85        self
86    }
87
88    /// 设置 incoming / outgoing 两侧可占用的最大长度。
89    #[must_use]
90    pub fn with_limits(mut self, incoming_limit: f64, outgoing_limit: f64) -> Self {
91        self.incoming_limit = incoming_limit;
92        self.outgoing_limit = outgoing_limit;
93        self
94    }
95
96    /// 解析 smooth corner 的几何参数。
97    pub fn geometry(&self) -> Result<SmoothCornerGeometry, SmoothError> {
98        if !self.origin.is_finite()
99            || !self.incoming_axis.is_finite()
100            || !self.outgoing_axis.is_finite()
101            || !self.radius.is_finite()
102            || !self.smoothing.is_finite()
103            || self.incoming_limit.is_nan()
104            || self.outgoing_limit.is_nan()
105        {
106            return Err(SmoothError::NonFiniteInput);
107        }
108        if self.radius < 0.0 || self.incoming_limit < 0.0 || self.outgoing_limit < 0.0 {
109            return Err(SmoothError::NegativeInput);
110        }
111
112        let incoming_axis = self
113            .incoming_axis
114            .normalized()
115            .ok_or(SmoothError::DegenerateAxis)?;
116        let outgoing_axis = self
117            .outgoing_axis
118            .normalized()
119            .ok_or(SmoothError::DegenerateAxis)?;
120        let angle = incoming_axis
121            .angle_between(outgoing_axis)
122            .ok_or(SmoothError::DegenerateAxis)?;
123
124        if angle <= EPSILON || PI - angle <= EPSILON {
125            return Err(SmoothError::InvalidAngle);
126        }
127
128        let smoothing = clamp01(self.smoothing);
129        let max_limit = self.incoming_limit.min(self.outgoing_limit);
130        let max_radius = max_limit * (angle / 2.0).tan();
131        let radius = self.radius.min(max_radius);
132
133        if radius <= EPSILON {
134            return Ok(SmoothCornerGeometry {
135                origin: self.origin,
136                incoming_axis,
137                outgoing_axis,
138                radius: 0.0,
139                smoothing,
140                incoming_limit: self.incoming_limit,
141                outgoing_limit: self.outgoing_limit,
142                angle,
143                base_tangent: 0.0,
144                incoming_influence: 0.0,
145                outgoing_influence: 0.0,
146                alpha0: 0.0,
147                alpha1: 0.0,
148                middle_arc_angle: angle,
149                start: self.origin,
150                end: self.origin,
151            });
152        }
153
154        let base_tangent = radius / (angle / 2.0).tan();
155        let raw_influence = (1.0 + smoothing) * base_tangent;
156        let incoming_influence = raw_influence.min(self.incoming_limit);
157        let outgoing_influence = raw_influence.min(self.outgoing_limit);
158
159        let alpha0 = clamp01(incoming_influence / base_tangent - 1.0) * angle / 2.0;
160        let alpha1 = clamp01(outgoing_influence / base_tangent - 1.0) * angle / 2.0;
161        let middle_arc_angle = (angle - alpha0 - alpha1).max(0.0);
162
163        Ok(SmoothCornerGeometry {
164            origin: self.origin,
165            incoming_axis,
166            outgoing_axis,
167            radius,
168            smoothing,
169            incoming_limit: self.incoming_limit,
170            outgoing_limit: self.outgoing_limit,
171            angle,
172            base_tangent,
173            incoming_influence,
174            outgoing_influence,
175            alpha0,
176            alpha1,
177            middle_arc_angle,
178            start: self.origin + incoming_axis * incoming_influence,
179            end: self.origin + outgoing_axis * outgoing_influence,
180        })
181    }
182
183    /// 生成最多 3 段 cubic Bezier。
184    ///
185    /// 半径为 0 或被限制压缩到 0 时返回空数组。
186    pub fn to_cubics(&self) -> Result<Vec<CubicSegment>, SmoothError> {
187        let geometry = self.geometry()?;
188        if geometry.radius <= EPSILON {
189            return Ok(Vec::new());
190        }
191
192        let center = geometry.origin
193            + (geometry.incoming_axis + geometry.outgoing_axis)
194                .normalized()
195                .ok_or(SmoothError::InvalidAngle)?
196                * (geometry.radius / (geometry.angle / 2.0).sin());
197        let incoming_tangent = geometry.origin + geometry.incoming_axis * geometry.base_tangent;
198        let e0 = (incoming_tangent - center) / geometry.radius;
199        let e1 = -geometry.incoming_axis;
200
201        let p1 = circle_point(center, geometry.radius, e0, e1, geometry.alpha0);
202        let p2 = circle_point(
203            center,
204            geometry.radius,
205            e0,
206            e1,
207            geometry.angle - geometry.alpha1,
208        );
209
210        let tangent0 = geometry.base_tangent - geometry.radius * (geometry.alpha0 / 2.0).tan();
211        let handle0 = (geometry.incoming_influence - tangent0) / 3.0;
212
213        let tangent1 = geometry.base_tangent - geometry.radius * (geometry.alpha1 / 2.0).tan();
214        let handle1 = (geometry.outgoing_influence - tangent1) / 3.0;
215
216        let arc_handle = if geometry.middle_arc_angle <= EPSILON {
217            0.0
218        } else {
219            (4.0 / 3.0) * (geometry.middle_arc_angle / 4.0).tan() * geometry.radius
220        };
221
222        let arc_tangent0 = circle_tangent(e0, e1, geometry.alpha0);
223        let arc_tangent1 = circle_tangent(e0, e1, geometry.angle - geometry.alpha1);
224
225        let c1 = CubicSegment {
226            from: geometry.start,
227            ctrl1: geometry.origin
228                + geometry.incoming_axis * (geometry.incoming_influence - 2.0 * handle0),
229            ctrl2: geometry.origin + geometry.incoming_axis * tangent0,
230            to: p1,
231        };
232        let c2 = CubicSegment {
233            from: p1,
234            ctrl1: p1 + arc_tangent0 * arc_handle,
235            ctrl2: p2 - arc_tangent1 * arc_handle,
236            to: p2,
237        };
238        let c3 = CubicSegment {
239            from: p2,
240            ctrl1: geometry.origin + geometry.outgoing_axis * tangent1,
241            ctrl2: geometry.origin + geometry.outgoing_axis * (tangent1 + handle1),
242            to: geometry.end,
243        };
244
245        Ok(vec![c1, c2, c3])
246    }
247}
248
249fn circle_point(center: Point, radius: f64, e0: Vector, e1: Vector, angle: f64) -> Point {
250    center + e0 * (radius * angle.cos()) + e1 * (radius * angle.sin())
251}
252
253fn circle_tangent(e0: Vector, e1: Vector, angle: f64) -> Vector {
254    -e0 * angle.sin() + e1 * angle.cos()
255}