1use ccalc_engine::env::Value;
4
5use crate::style::{
6 StyleColor, StyleSpec, looks_like_style_str, parse_color_token, parse_style_str,
7};
8
9pub fn extract_file_arg(args: &[Value]) -> (Vec<Value>, Option<String>) {
15 if let Some(last) = args.last()
16 && let Some(s) = as_str(last)
17 && (s == "ascii" || s.ends_with(".svg") || s.ends_with(".png"))
18 {
19 return (args[..args.len() - 1].to_vec(), Some(s));
20 }
21 (args.to_vec(), None)
22}
23
24pub fn extract_flat(v: &Value) -> Result<Vec<f64>, String> {
31 match v {
32 Value::Scalar(f) => Ok(vec![*f]),
33 Value::Matrix(m) => Ok(m.iter().copied().collect()),
34 _ => Err("plot: numeric array argument required".into()),
35 }
36}
37
38pub fn extract_vector(v: &Value) -> Result<Vec<f64>, String> {
43 match v {
44 Value::Scalar(f) => Ok(vec![*f]),
45 Value::Matrix(m) => {
46 let (r, c) = (m.nrows(), m.ncols());
47 if r == 1 || c == 1 {
48 Ok(m.iter().copied().collect())
49 } else {
50 Err(format!("plot: expected a vector, got {r}×{c} matrix"))
51 }
52 }
53 _ => Err("plot: numeric vector argument required".into()),
54 }
55}
56
57pub fn extract_matrix(v: &Value) -> Result<(Vec<f64>, usize, usize), String> {
62 match v {
63 Value::Matrix(m) => {
64 let nrows = m.nrows();
65 let ncols = m.ncols();
66 let mut data = Vec::with_capacity(nrows * ncols);
67 for r in 0..nrows {
68 for c in 0..ncols {
69 data.push(m[[r, c]]);
70 }
71 }
72 Ok((data, nrows, ncols))
73 }
74 Value::Scalar(f) => Ok((vec![*f], 1, 1)),
75 _ => Err("imagesc: expected a numeric matrix".into()),
76 }
77}
78
79#[allow(clippy::type_complexity)]
81pub fn extract_style_and_file_arg(
96 args: &[Value],
97) -> Result<(Vec<Value>, Option<StyleSpec>, Option<String>), String> {
98 extract_style_and_file_arg_min(args, 1)
99}
100
101#[allow(clippy::type_complexity)]
103pub fn extract_style_and_file_arg_min(
104 args: &[Value],
105 min_data: usize,
106) -> Result<(Vec<Value>, Option<StyleSpec>, Option<String>), String> {
107 let (mut data_args, path) = extract_file_arg(args);
108
109 let mut extra_lw: Option<f32> = None;
113 let mut extra_ms: Option<u32> = None;
114 loop {
115 let len = data_args.len();
116 if len < 2 {
117 break;
118 }
119 if let Some(key) = as_str(&data_args[len - 2]) {
120 if key.eq_ignore_ascii_case("linewidth") {
121 extra_lw = Some(match &data_args[len - 1] {
122 Value::Scalar(f) if *f > 0.0 => *f as f32,
123 _ => return Err("linewidth: value must be a positive number".into()),
124 });
125 data_args.truncate(len - 2);
126 continue;
127 }
128 if key.eq_ignore_ascii_case("markersize") {
129 extra_ms = Some(match &data_args[len - 1] {
130 Value::Scalar(f) if *f >= 1.0 => *f as u32,
131 _ => return Err("markersize: value must be a positive integer".into()),
132 });
133 data_args.truncate(len - 2);
134 continue;
135 }
136 }
137 break;
138 }
139
140 let len = data_args.len();
142 if len >= 2
143 && let Some(key) = as_str(&data_args[len - 2])
144 && key.eq_ignore_ascii_case("color")
145 {
146 let sc = value_to_style_color(&data_args[len - 1])?;
147 data_args.truncate(len - 2);
148 return Ok((
149 data_args,
150 Some(StyleSpec {
151 color: Some(sc),
152 line_width: extra_lw,
153 marker_size: extra_ms,
154 ..StyleSpec::default()
155 }),
156 path,
157 ));
158 }
159
160 let rgb_style = if data_args.len() > min_data {
164 if let Some(Value::Matrix(m)) = data_args.last() {
165 if m.nrows() == 1 && m.ncols() == 3 && m.iter().all(|&v| (0.0..=1.0).contains(&v)) {
166 let clamp = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
167 Some(StyleColor(
168 clamp(m[[0, 0]]),
169 clamp(m[[0, 1]]),
170 clamp(m[[0, 2]]),
171 ))
172 } else {
173 None
174 }
175 } else {
176 None
177 }
178 } else {
179 None
180 };
181 if let Some(sc) = rgb_style {
182 data_args.pop();
183 return Ok((
184 data_args,
185 Some(StyleSpec {
186 color: Some(sc),
187 line_width: extra_lw,
188 marker_size: extra_ms,
189 ..StyleSpec::default()
190 }),
191 path,
192 ));
193 }
194
195 let mut style: Option<StyleSpec> = None;
197 if let Some(last) = data_args.last()
198 && let Some(s) = as_str(last)
199 && looks_like_style_str(&s)
200 {
201 let mut sp = parse_style_str(&s)?;
202 sp.line_width = extra_lw;
203 sp.marker_size = extra_ms;
204 style = Some(sp);
205 data_args.pop();
206 } else if extra_lw.is_some() || extra_ms.is_some() {
207 style = Some(StyleSpec {
208 line_width: extra_lw,
209 marker_size: extra_ms,
210 ..StyleSpec::default()
211 });
212 }
213
214 Ok((data_args, style, path))
215}
216
217fn value_to_style_color(v: &Value) -> Result<StyleColor, String> {
219 match v {
220 Value::Str(s) | Value::StringObj(s) => parse_color_token(s)
221 .ok_or_else(|| format!("plot: '{s}' is not a recognised color name or hex code")),
222 Value::Matrix(m) if m.nrows() == 1 && m.ncols() == 3 => {
223 let clamp = |v: f64| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
224 Ok(StyleColor(
225 clamp(m[[0, 0]]),
226 clamp(m[[0, 1]]),
227 clamp(m[[0, 2]]),
228 ))
229 }
230 _ => Err("plot: 'color' value must be a color name string or 1×3 matrix".into()),
231 }
232}
233
234fn as_str(v: &Value) -> Option<String> {
235 match v {
236 Value::Str(s) | Value::StringObj(s) => Some(s.clone()),
237 _ => None,
238 }
239}