Skip to main content

smooth_frame/
path.rs

1use crate::geometry::Point;
2
3/// 一段 cubic Bezier,包含起点,便于直接映射到底层渲染 API。
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct CubicSegment {
6    /// cubic 起点。
7    pub from: Point,
8    /// 第一个控制点。
9    pub ctrl1: Point,
10    /// 第二个控制点。
11    pub ctrl2: Point,
12    /// cubic 终点。
13    pub to: Point,
14}
15
16/// 可直接映射到 SVG Canvas Skia 等 API 的路径命令。
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum PathCommand {
19    /// 移动当前点。
20    MoveTo(Point),
21    /// 从当前点绘制直线。
22    LineTo(Point),
23    /// 从当前点绘制 cubic Bezier。
24    CubicTo {
25        /// 第一个控制点。
26        ctrl1: Point,
27        /// 第二个控制点。
28        ctrl2: Point,
29        /// cubic 终点。
30        to: Point,
31    },
32    /// 闭合当前子路径。
33    Close,
34}
35
36/// 平滑路径。
37#[derive(Debug, Clone, PartialEq, Default)]
38pub struct SmoothPath {
39    commands: Vec<PathCommand>,
40}
41
42impl SmoothPath {
43    /// 创建空路径。
44    #[must_use]
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// 返回底层路径命令。
50    #[must_use]
51    pub fn commands(&self) -> &[PathCommand] {
52        &self.commands
53    }
54
55    /// 提取路径中的所有 cubic 段。
56    #[must_use]
57    pub fn cubics(&self) -> Vec<CubicSegment> {
58        let mut cubics = Vec::new();
59        let mut current = None;
60        let mut subpath_start = None;
61
62        for command in &self.commands {
63            match *command {
64                PathCommand::MoveTo(point) => {
65                    current = Some(point);
66                    subpath_start = Some(point);
67                }
68                PathCommand::LineTo(point) => {
69                    current = Some(point);
70                }
71                PathCommand::CubicTo { ctrl1, ctrl2, to } => {
72                    if let Some(from) = current {
73                        cubics.push(CubicSegment {
74                            from,
75                            ctrl1,
76                            ctrl2,
77                            to,
78                        });
79                    }
80                    current = Some(to);
81                }
82                PathCommand::Close => {
83                    current = subpath_start;
84                }
85            }
86        }
87
88        cubics
89    }
90
91    /// 追加 `MoveTo` 命令。
92    pub fn move_to(&mut self, point: Point) {
93        self.commands.push(PathCommand::MoveTo(point));
94    }
95
96    /// 追加 `LineTo` 命令。
97    pub fn line_to(&mut self, point: Point) {
98        self.commands.push(PathCommand::LineTo(point));
99    }
100
101    /// 追加 `CubicTo` 命令。
102    pub fn cubic_to(&mut self, ctrl1: Point, ctrl2: Point, to: Point) {
103        self.commands
104            .push(PathCommand::CubicTo { ctrl1, ctrl2, to });
105    }
106
107    /// 追加闭合路径命令。
108    pub fn close(&mut self) {
109        self.commands.push(PathCommand::Close);
110    }
111
112    /// 以默认 6 位小数输出 SVG path data。
113    #[must_use]
114    pub fn to_svg_path(&self) -> String {
115        self.to_svg_path_with_precision(6)
116    }
117
118    /// 按指定小数位数输出 SVG path data。
119    #[must_use]
120    pub fn to_svg_path_with_precision(&self, precision: usize) -> String {
121        let mut parts = Vec::with_capacity(self.commands.len());
122        for command in &self.commands {
123            match *command {
124                PathCommand::MoveTo(point) => {
125                    parts.push(format!("M {}", format_point(point, precision)));
126                }
127                PathCommand::LineTo(point) => {
128                    parts.push(format!("L {}", format_point(point, precision)));
129                }
130                PathCommand::CubicTo { ctrl1, ctrl2, to } => {
131                    parts.push(format!(
132                        "C {} {} {}",
133                        format_point(ctrl1, precision),
134                        format_point(ctrl2, precision),
135                        format_point(to, precision)
136                    ));
137                }
138                PathCommand::Close => parts.push("Z".to_owned()),
139            }
140        }
141        parts.join(" ")
142    }
143}
144
145fn format_point(point: Point, precision: usize) -> String {
146    format!(
147        "{},{}",
148        format_number(point.x, precision),
149        format_number(point.y, precision)
150    )
151}
152
153fn format_number(value: f64, precision: usize) -> String {
154    let zero_epsilon = 10.0_f64.powi(-(precision as i32 + 1));
155    let value = if value.abs() < zero_epsilon {
156        0.0
157    } else {
158        value
159    };
160    let mut text = format!("{value:.precision$}");
161    if text.contains('.') {
162        while text.ends_with('0') {
163            text.pop();
164        }
165        if text.ends_with('.') {
166            text.pop();
167        }
168    }
169    if text == "-0" { "0".to_owned() } else { text }
170}