1use std::env;
2use std::fs;
3use std::path::PathBuf;
4use std::process;
5
6use smooth_frame::SmoothRect;
7
8#[derive(Debug, Clone)]
9struct Config {
10 width: f64,
11 height: f64,
12 radius: Option<f64>,
13 smoothing: f64,
14 precision: usize,
15 border_width: f64,
16 svg: bool,
17 output: Option<PathBuf>,
18}
19
20impl Default for Config {
21 fn default() -> Self {
22 Self {
23 width: 1000.0,
24 height: 1000.0,
25 radius: Some(225.0),
26 smoothing: 0.6,
27 precision: 3,
28 border_width: 0.0,
29 svg: false,
30 output: None,
31 }
32 }
33}
34
35fn main() {
36 let config = match parse_args(env::args().skip(1)) {
37 Ok(Some(config)) => config,
38 Ok(None) => return,
39 Err(message) => {
40 eprintln!("参数错误:{message}");
41 eprintln!("运行 `cargo run --example demo -- --help` 查看用法。");
42 process::exit(2);
43 }
44 };
45
46 let drawable_width = drawable_dimension(config.width, config.border_width);
47 let drawable_height = drawable_dimension(config.height, config.border_width);
48 let radius = effective_radius(&config);
49 let path = SmoothRect::new(drawable_width, drawable_height)
50 .with_radius(radius)
51 .with_smoothing(config.smoothing)
52 .to_path();
53 let path_data = path.to_svg_path_with_precision(config.precision);
54
55 if let Some(output) = config.output.as_ref() {
56 let svg = render_svg(&config, &path_data);
57 if let Err(error) = fs::write(output, svg) {
58 eprintln!("写入 SVG 文件失败:{}:{error}", output.display());
59 process::exit(1);
60 }
61 println!("已生成 SVG 文件:{}", output.display());
62 } else if config.svg {
63 println!("{}", render_svg(&config, &path_data));
64 } else {
65 println!("SVG 宽度:{}", format_number(config.width));
66 println!("SVG 高度:{}", format_number(config.height));
67 println!("路径宽度:{}", format_number(drawable_width));
68 println!("路径高度:{}", format_number(drawable_height));
69 println!("半径:{}", format_number(radius));
70 println!("平滑:{}", format_number(config.smoothing));
71 println!("边框:{}", format_number(config.border_width));
72 println!("命令数:{}", path.commands().len());
73 println!("SVG path:{path_data}");
74 }
75}
76
77fn parse_args(args: impl Iterator<Item = String>) -> Result<Option<Config>, String> {
78 let mut config = Config::default();
79 let mut args = args.peekable();
80
81 while let Some(arg) = args.next() {
82 match arg.as_str() {
83 "-h" | "--help" => {
84 print_help();
85 return Ok(None);
86 }
87 "--svg" => config.svg = true,
88 "-o" | "--output" => {
89 config.output = Some(parse_path_arg("--output", args.next())?);
90 }
91 "--width" => config.width = parse_f64_arg("--width", args.next())?,
92 "--height" => config.height = parse_f64_arg("--height", args.next())?,
93 "--radius" => config.radius = Some(parse_f64_arg("--radius", args.next())?),
94 "--smoothing" => config.smoothing = parse_f64_arg("--smoothing", args.next())?,
95 "--precision" => config.precision = parse_usize_arg("--precision", args.next())?,
96 "--border" | "--border-width" => {
97 config.border_width = parse_f64_arg("--border-width", args.next())?;
98 }
99 _ if arg.starts_with("--width=") => {
100 config.width = parse_f64_value("--width", value_after_equals(&arg))?;
101 }
102 _ if arg.starts_with("--height=") => {
103 config.height = parse_f64_value("--height", value_after_equals(&arg))?;
104 }
105 _ if arg.starts_with("--radius=") => {
106 config.radius = Some(parse_f64_value("--radius", value_after_equals(&arg))?);
107 }
108 _ if arg.starts_with("--smoothing=") => {
109 config.smoothing = parse_f64_value("--smoothing", value_after_equals(&arg))?;
110 }
111 _ if arg.starts_with("--precision=") => {
112 config.precision = parse_usize_value("--precision", value_after_equals(&arg))?;
113 }
114 _ if arg.starts_with("--border=") => {
115 config.border_width = parse_f64_value("--border", value_after_equals(&arg))?;
116 }
117 _ if arg.starts_with("--border-width=") => {
118 config.border_width = parse_f64_value("--border-width", value_after_equals(&arg))?;
119 }
120 _ if arg.starts_with("--output=") => {
121 config.output = Some(PathBuf::from(value_after_equals(&arg)));
122 }
123 _ => return Err(format!("未知参数 `{arg}`")),
124 }
125 }
126
127 validate_config(&config)?;
128 Ok(Some(config))
129}
130
131fn validate_config(config: &Config) -> Result<(), String> {
132 if !config.width.is_finite() || config.width <= 0.0 {
133 return Err("--width 必须是大于 0 的有限数字".to_owned());
134 }
135 if !config.height.is_finite() || config.height <= 0.0 {
136 return Err("--height 必须是大于 0 的有限数字".to_owned());
137 }
138 if let Some(radius) = config.radius {
139 if !radius.is_finite() || radius < 0.0 {
140 return Err("--radius 必须是大于或等于 0 的有限数字".to_owned());
141 }
142 }
143 if !config.smoothing.is_finite() || !(0.0..=1.0).contains(&config.smoothing) {
144 return Err("--smoothing 必须在 0 到 1 之间".to_owned());
145 }
146 if !config.border_width.is_finite() || config.border_width < 0.0 {
147 return Err("--border-width 必须是大于或等于 0 的有限数字".to_owned());
148 }
149 if config.border_width >= config.width.min(config.height) {
150 return Err("--border-width 必须小于 SVG 宽度和高度".to_owned());
151 }
152 if config.precision > 12 {
153 return Err("--precision 不能大于 12".to_owned());
154 }
155 Ok(())
156}
157
158fn effective_radius(config: &Config) -> f64 {
159 config.radius.unwrap_or(250.0)
160}
161
162fn drawable_dimension(svg_dimension: f64, border_width: f64) -> f64 {
163 svg_dimension - border_width
164}
165
166fn parse_f64_arg(name: &str, value: Option<String>) -> Result<f64, String> {
167 parse_f64_value(
168 name,
169 value
170 .as_deref()
171 .ok_or_else(|| format!("缺少 {name} 的值"))?,
172 )
173}
174
175fn parse_usize_arg(name: &str, value: Option<String>) -> Result<usize, String> {
176 parse_usize_value(
177 name,
178 value
179 .as_deref()
180 .ok_or_else(|| format!("缺少 {name} 的值"))?,
181 )
182}
183
184fn parse_path_arg(name: &str, value: Option<String>) -> Result<PathBuf, String> {
185 value
186 .map(PathBuf::from)
187 .ok_or_else(|| format!("缺少 {name} 的值"))
188}
189
190fn parse_f64_value(name: &str, value: &str) -> Result<f64, String> {
191 value
192 .parse()
193 .map_err(|_| format!("{name} 的值 `{value}` 不是有效数字"))
194}
195
196fn parse_usize_value(name: &str, value: &str) -> Result<usize, String> {
197 value
198 .parse()
199 .map_err(|_| format!("{name} 的值 `{value}` 不是有效整数"))
200}
201
202fn value_after_equals(arg: &str) -> &str {
203 arg.split_once('=').map_or("", |(_, value)| value)
204}
205
206fn render_svg(config: &Config, path_data: &str) -> String {
207 let offset = config.border_width / 2.0;
208 let stroke_attrs = if config.border_width > 0.0 {
209 format!(
210 r##" stroke="#0F172A" stroke-opacity="0.12" stroke-width="{}""##,
211 format_number(config.border_width)
212 )
213 } else {
214 String::new()
215 };
216
217 format!(
218 r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {canvas_width} {canvas_height}" width="{canvas_width}" height="{canvas_height}">
219 <defs>
220 <linearGradient id="smooth-frame-fill" x1="0" y1="0" x2="1" y2="1">
221 <stop offset="0%" stop-color="#0EA5E9"/>
222 <stop offset="48%" stop-color="#2DD4BF"/>
223 <stop offset="100%" stop-color="#FDE68A"/>
224 </linearGradient>
225 </defs>
226 <path d="{path_data}" transform="translate({offset} {offset})" fill="url(#smooth-frame-fill)"{stroke_attrs}/>
227</svg>"##,
228 canvas_width = format_number(config.width),
229 canvas_height = format_number(config.height),
230 offset = format_number(offset),
231 )
232}
233
234fn format_number(value: f64) -> String {
235 let formatted = format!("{value:.3}");
236 formatted
237 .trim_end_matches('0')
238 .trim_end_matches('.')
239 .to_owned()
240}
241
242fn print_help() {
243 println!(
244 r#"demo
245
246生成 Sketch-like smooth corner 矩形的 SVG path 或完整 SVG。
247
248用法:
249 cargo run --example demo
250 cargo run --example demo -- --width 1000 --height 1000 --radius 250 --smoothing 0.6
251 cargo run --example demo -- --svg > smooth.svg
252 cargo run --example demo -- --output smooth.svg
253
254参数:
255 --width <数字> 矩形宽度,默认 1000
256 --height <数字> 矩形高度,默认 1000
257 --radius <数字> 核心圆半径,默认 250
258 --smoothing <数字> 平滑系数,范围 0..1,默认 0.6
259 --precision <整数> SVG path 小数位数,默认 3
260 --border <数字> SVG 边框宽度,默认 0
261 --svg 输出完整 SVG,而不是只输出 path
262 -o, --output <路径> 生成完整 SVG 文件
263 -h, --help 显示帮助
264"#
265 );
266}