1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
3pub struct TrackFlags {
4 ease_in: bool,
6 ease_out: bool,
8 twopoints: bool,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
14pub struct TrackStep(pub u64);
15
16#[derive(Debug, Clone, thiserror::Error)]
18#[non_exhaustive]
19pub enum TrackStepParseError {
20 #[error("invalid track step precision")]
21 InvalidPrecision,
22 #[error("failed to parse track step value")]
23 ParseError(#[from] std::num::ParseFloatError),
24}
25
26impl TrackStep {
27 pub fn parse_and_get(value: &str) -> Result<(TrackStep, f64), TrackStepParseError> {
29 let maybe_negative = value.starts_with('-');
30 let abs_value_str = if maybe_negative { &value[1..] } else { value };
31 let v: f64 = abs_value_str.parse()?;
32 let dot_index = abs_value_str.find('.');
33 let step = TrackStep(match dot_index {
34 None => 0,
35 Some(idx) => (abs_value_str.len() - idx - 1)
36 .try_into()
37 .map_err(|_| TrackStepParseError::InvalidPrecision)?,
38 });
39 let final_value = if maybe_negative { -v } else { v };
40 Ok((step, final_value))
41 }
42
43 pub fn round_to_string(&self, value: f64) -> String {
45 match self.0 {
46 0 => format!("{}", value.round() as i64),
47 precision => format!("{:.*}", precision as usize, value),
48 }
49 }
50}
51
52impl TryFrom<f64> for TrackStep {
53 type Error = ();
54
55 fn try_from(value: f64) -> Result<Self, Self::Error> {
56 if value == 1.0 {
57 return Ok(TrackStep(0));
58 }
59 if !(value > 0.0 && value < 1.0) {
60 return Err(());
61 }
62
63 let mut current = value;
64 let mut precision = 0u64;
65 loop {
66 if (current - 1.0).abs() < f64::EPSILON {
67 return Ok(TrackStep(precision));
68 }
69 if current > 1.0 || precision >= 15 {
70 return Err(());
71 }
72 current *= 10.0;
73 precision += 1;
74 }
75 }
76}
77impl From<TrackStep> for f64 {
78 fn from(step: TrackStep) -> Self {
79 10f64.powi(-(step.0 as i32))
80 }
81}
82impl TrackStep {
83 pub fn value(&self) -> f64 {
85 (*self).into()
86 }
87}
88
89impl std::fmt::Display for TrackStep {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self.0 {
92 0 => write!(f, "1"),
93 precision => write!(f, "0.{}1", "0".repeat(precision as usize - 1)),
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq)]
100pub struct TimeCurve {
101 pub control_points: Vec<TimeCurvePoint>,
103}
104
105impl std::fmt::Display for TimeCurve {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 if self.control_points.is_empty() {
108 return write!(f, "");
109 }
110 if self.control_points.len() == 2
111 && self.control_points[0].position == 0.0
112 && self.control_points[0].value == 0.0
113 && self.control_points[1].position == 1.0
114 && self.control_points[1].value == 1.0
115 {
116 return write!(
117 f,
118 "{},{},{},{}",
119 self.control_points[0].right_handle.0,
120 self.control_points[0].right_handle.1,
121 self.control_points[1].right_handle.0,
122 self.control_points[1].right_handle.1
123 );
124 }
125 let parts: Vec<String> = self
126 .control_points
127 .iter()
128 .map(|pt| {
129 format!(
130 "{},{},{},{}",
131 pt.position, pt.value, pt.right_handle.0, pt.right_handle.1
132 )
133 })
134 .collect();
135 write!(f, "{}", parts.join(","))
136 }
137}
138
139#[derive(Debug, Clone, PartialEq)]
141pub struct TimeCurvePoint {
142 pub position: f64,
144 pub value: f64,
146 pub right_handle: (f64, f64),
152}
153
154impl Default for TimeCurve {
155 fn default() -> Self {
156 TimeCurve {
157 control_points: vec![
158 TimeCurvePoint {
159 position: 0.0,
160 value: 0.0,
161 right_handle: (0.25, 0.25),
162 },
163 TimeCurvePoint {
164 position: 1.0,
165 value: 1.0,
166 right_handle: (0.25, 0.25),
167 },
168 ],
169 }
170 }
171}
172
173#[derive(Debug, Clone, thiserror::Error)]
175#[non_exhaustive]
176pub enum TimeCurveParseError {
177 #[error("invalid format")]
178 InvalidFormat,
179
180 #[error("invalid number of components ({0})")]
181 InvalidNumComponents(usize),
182
183 #[error("value out of range")]
184 ValueOutOfRange,
185
186 #[error("control points are not in increasing order")]
187 BadPositionOrder,
188}
189
190impl std::str::FromStr for TimeCurve {
191 type Err = TimeCurveParseError;
192
193 fn from_str(s: &str) -> Result<Self, Self::Err> {
194 let parts: Vec<f64> = s
195 .split(',')
196 .map(|part| {
197 part.parse::<f64>()
198 .map_err(|_| TimeCurveParseError::InvalidFormat)
199 })
200 .collect::<Result<_, _>>()?;
201 match parts.len() {
202 n if n % 4 != 0 => Err(TimeCurveParseError::InvalidNumComponents(n)),
203 0 => Ok(TimeCurve::default()),
204 4 => Ok(TimeCurve {
205 control_points: vec![
206 TimeCurvePoint {
207 position: 0.0,
208 value: 0.0,
209 right_handle: (parts[0], parts[1]),
210 },
211 TimeCurvePoint {
212 position: 1.0,
213 value: 1.0,
214 right_handle: (parts[2], parts[3]),
215 },
216 ],
217 }),
218 _ => {
219 let control_points = parts
220 .chunks(4)
221 .map(|chunk| {
222 if chunk[0] < 0.0
223 || chunk[0] > 1.0
224 || chunk[1] < 0.0
225 || chunk[1] > 1.0
226 || chunk[2] < 0.0
227 {
228 return Err(TimeCurveParseError::ValueOutOfRange);
229 }
230 Ok(TimeCurvePoint {
231 position: chunk[0],
232 value: chunk[1],
233 right_handle: (chunk[2], chunk[3]),
234 })
235 })
236 .collect::<Result<Vec<_>, _>>()?;
237 if !control_points
238 .windows(2)
239 .all(|w| w[0].position < w[1].position)
240 {
241 return Err(TimeCurveParseError::BadPositionOrder);
242 }
243 Ok(TimeCurve { control_points })
244 }
245 }
246 }
247}
248
249#[derive(Debug, Clone, PartialEq)]
251pub enum TrackItem {
252 Static(StaticTrackItem),
254 Animated(AnimatedTrackItem),
256}
257
258impl std::fmt::Display for TrackItem {
259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260 match self {
261 TrackItem::Static(item) => write!(f, "{}", item),
262 TrackItem::Animated(item) => write!(f, "{}", item),
263 }
264 }
265}
266
267impl std::str::FromStr for TrackItem {
268 type Err = TrackItemParseError;
269
270 fn from_str(s: &str) -> Result<Self, Self::Err> {
271 if s.contains(',') {
272 let animated: AnimatedTrackItem = s.parse()?;
273 Ok(TrackItem::Animated(animated))
274 } else {
275 let static_item: StaticTrackItem = s.parse()?;
276 Ok(TrackItem::Static(static_item))
277 }
278 }
279}
280
281#[derive(Debug, Clone, PartialEq)]
283pub struct StaticTrackItem {
284 pub step: TrackStep,
286
287 pub value: f64,
289}
290
291impl std::fmt::Display for StaticTrackItem {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 let value_str = self.step.round_to_string(self.value);
294 write!(f, "{}", value_str)
295 }
296}
297
298impl std::str::FromStr for StaticTrackItem {
299 type Err = TrackStepParseError;
300
301 fn from_str(s: &str) -> Result<Self, Self::Err> {
302 let (step, value) = TrackStep::parse_and_get(s)?;
303 Ok(StaticTrackItem { step, value })
304 }
305}
306
307#[derive(Debug, Clone, PartialEq)]
309pub struct AnimatedTrackItem {
310 pub step: TrackStep,
312 pub values: Vec<f64>,
314
315 pub flags: TrackFlags,
317 pub script_name: String,
319 pub parameter: Option<f64>,
321 pub time_curve: Option<TimeCurve>,
323}
324
325#[derive(Debug, Clone, thiserror::Error)]
327#[non_exhaustive]
328pub enum TrackItemParseError {
329 #[error("invalid segments count")]
330 InvalidNumSegments(usize),
331
332 #[error("invalid elements count")]
333 InvalidNumElements(usize),
334
335 #[error("failed to parse element")]
336 ElementParseError(#[from] std::num::ParseFloatError),
337
338 #[error("failed to parse curve")]
339 TimeCurveParseError(#[from] TimeCurveParseError),
340
341 #[error("invalid flag value")]
342 InvalidFlagValue,
343
344 #[error("inconsistent step")]
345 InconsistentStep,
346
347 #[error("invalid step value")]
348 InvalidStepValue(#[from] TrackStepParseError),
349}
350
351impl std::fmt::Display for AnimatedTrackItem {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 let mut elements = Vec::new();
354 for value in &self.values {
355 elements.push(self.step.round_to_string(*value));
356 }
357 elements.push(self.script_name.clone());
358 let flags_value = (if self.flags.ease_in { 0b0001 } else { 0 })
359 | (if self.flags.ease_out { 0b0010 } else { 0 })
360 | (if self.flags.twopoints { 0b0100 } else { 0 });
361 elements.push(flags_value.to_string());
362 let mut result = elements.join(",");
363 if let Some(param) = self.parameter {
364 result.push('|');
365 result.push_str(¶m.to_string());
366 }
367 if let Some(curve) = &self.time_curve {
368 result.push('|');
369 result.push_str(&curve.to_string());
370 }
371 write!(f, "{}", result)
372 }
373}
374
375impl std::str::FromStr for AnimatedTrackItem {
376 type Err = TrackItemParseError;
377
378 fn from_str(s: &str) -> Result<Self, Self::Err> {
379 let segments = s.split('|').collect::<Vec<&str>>();
380 let items = segments[0].split(",").collect::<Vec<&str>>();
381 if items.len() < 4 {
382 return Err(TrackItemParseError::InvalidNumElements(items.len()));
383 }
384 let flags_value: u8 = items[items.len() - 1]
385 .parse()
386 .map_err(|_| TrackItemParseError::InvalidFlagValue)?;
387 let flags = TrackFlags {
388 ease_in: (flags_value & 0b0001) != 0,
389 ease_out: (flags_value & 0b0010) != 0,
390 twopoints: (flags_value & 0b0100) != 0,
391 };
392 let (parameter, time_curve) = match segments.len() {
393 1 => (None, None),
394 2 if segments[1].contains(',') => {
395 let time_curve: TimeCurve = segments[1]
396 .parse()
397 .map_err(TrackItemParseError::TimeCurveParseError)?;
398 (None, Some(time_curve))
399 }
400 2 => {
401 let parameter: f64 = segments[1]
402 .parse()
403 .map_err(TrackItemParseError::ElementParseError)?;
404 (Some(parameter), None)
405 }
406 3 => {
407 let parameter: f64 = segments[1]
408 .parse()
409 .map_err(TrackItemParseError::ElementParseError)?;
410 let time_curve: TimeCurve = segments[2]
411 .parse()
412 .map_err(TrackItemParseError::TimeCurveParseError)?;
413 (Some(parameter), Some(time_curve))
414 }
415 n => {
416 return Err(TrackItemParseError::InvalidNumSegments(n));
417 }
418 };
419
420 let script_name = items[items.len() - 2].to_string();
421 let (step, _) = TrackStep::parse_and_get(items[0])?;
422 let mut values = Vec::new();
423 for item in &items[0..items.len() - 2] {
424 let (item_step, value) = TrackStep::parse_and_get(item)?;
425 if item_step != step {
426 return Err(TrackItemParseError::InconsistentStep);
427 }
428 values.push(value);
429 }
430 Ok(AnimatedTrackItem {
431 step,
432 values,
433 flags,
434 script_name,
435 parameter,
436 time_curve,
437 })
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use rstest::rstest;
445
446 #[test]
447 fn test_time_curve_from_str() {
448 let tc: TimeCurve = "0.25,0.25,0.25,0.25".parse().unwrap();
449 assert_eq!(
450 tc.control_points,
451 vec![
452 TimeCurvePoint {
453 position: 0.0,
454 value: 0.0,
455 right_handle: (0.25, 0.25),
456 },
457 TimeCurvePoint {
458 position: 1.0,
459 value: 1.0,
460 right_handle: (0.25, 0.25),
461 },
462 ]
463 );
464 assert_eq!(tc.to_string(), "0.25,0.25,0.25,0.25");
465 }
466
467 #[test]
468 fn test_time_curve_from_str_multiple_points() {
469 let tc: TimeCurve = "0,0,0.25,0,0.5,0.5,0.25,0,1,1,0.25,0".parse().unwrap();
470
471 assert_eq!(
472 tc.control_points,
473 vec![
474 TimeCurvePoint {
475 position: 0.0,
476 value: 0.0,
477 right_handle: (0.25, 0.0),
478 },
479 TimeCurvePoint {
480 position: 0.5,
481 value: 0.5,
482 right_handle: (0.25, 0.0),
483 },
484 TimeCurvePoint {
485 position: 1.0,
486 value: 1.0,
487 right_handle: (0.25, 0.0),
488 },
489 ]
490 );
491
492 assert_eq!(tc.to_string(), "0,0,0.25,0,0.5,0.5,0.25,0,1,1,0.25,0");
493 }
494
495 #[test]
496 fn test_track_item_parse() {
497 let item_str = "0.1,0.2,MyScript,3|1.5|0.25,0.25,0.25,0.25";
498 let animated_item: AnimatedTrackItem = item_str.parse().unwrap();
499 assert_eq!(
500 animated_item,
501 AnimatedTrackItem {
502 step: TrackStep(1),
503 values: vec![0.1, 0.2],
504 flags: TrackFlags {
505 ease_in: true,
506 ease_out: true,
507 twopoints: false,
508 },
509 script_name: "MyScript".to_string(),
510 parameter: Some(1.5),
511 time_curve: Some(TimeCurve {
512 control_points: vec![
513 TimeCurvePoint {
514 position: 0.0,
515 value: 0.0,
516 right_handle: (0.25, 0.25),
517 },
518 TimeCurvePoint {
519 position: 1.0,
520 value: 1.0,
521 right_handle: (0.25, 0.25),
522 },
523 ],
524 }),
525 }
526 );
527 assert_eq!(animated_item.to_string(), item_str);
528 }
529 #[test]
530 fn test_track_item_parse_segments() {
531 let item_str = "0.1,0.2,MyScript,3|1.5|0.25,0.25,0.25,0.25";
532 let animated_item: AnimatedTrackItem = item_str.parse().unwrap();
533 assert_eq!(animated_item.parameter, Some(1.5));
534 assert!(animated_item.time_curve.is_some());
535
536 let item_str_no_curve = "0.1,0.2,MyScript,3|1.5";
537 let animated_item_no_curve: AnimatedTrackItem = item_str_no_curve.parse().unwrap();
538 assert_eq!(animated_item_no_curve.parameter, Some(1.5));
539 assert!(animated_item_no_curve.time_curve.is_none());
540
541 let item_str_no_param = "0.1,0.2,MyScript,3|0.25,0.25,0.25,0.25";
542 let animated_item_no_param: AnimatedTrackItem = item_str_no_param.parse().unwrap();
543 assert!(animated_item_no_param.parameter.is_none());
544 assert!(animated_item_no_param.time_curve.is_some());
545
546 let item_str_only_values = "0.1,0.2,MyScript,3";
547 let animated_item_only_values: AnimatedTrackItem = item_str_only_values.parse().unwrap();
548 assert!(animated_item_only_values.parameter.is_none());
549 assert!(animated_item_only_values.time_curve.is_none());
550 }
551
552 #[rstest]
553 #[case("1", TrackStep(0), 1.0)]
554 #[case("0.1", TrackStep(1), 0.1)]
555 #[case("0.01", TrackStep(2), 0.01)]
556 #[case("0.001", TrackStep(3), 0.001)]
557 #[case("-2.34", TrackStep(2), -2.34)]
558 fn test_track_step_parse_and_get(
559 #[case] input: &str,
560 #[case] expected_step: TrackStep,
561 #[case] expected_value: f64,
562 ) {
563 let (step, value) = TrackStep::parse_and_get(input).unwrap();
564 assert_eq!(step, expected_step);
565 assert_eq!(value, expected_value);
566 }
567
568 #[rstest]
569 #[case(TrackStep(0), 2.34, "2")]
570 #[case(TrackStep(1), 2.34, "2.3")]
571 #[case(TrackStep(2), 2.345, "2.35")]
572 #[case(TrackStep(3), 2.3456, "2.346")]
573 #[case(TrackStep(2), -2.345, "-2.35")]
574 #[case(TrackStep(0), -2.34, "-2")]
575 fn test_track_step_round_to_string(
576 #[case] step: TrackStep,
577 #[case] value: f64,
578 #[case] expected_str: &str,
579 ) {
580 let result_str = step.round_to_string(value);
581 assert_eq!(result_str, expected_str);
582 }
583
584 #[rstest]
585 #[case(1.0, TrackStep(0))]
586 #[case(0.1, TrackStep(1))]
587 #[case(0.01, TrackStep(2))]
588 #[case(0.001, TrackStep(3))]
589 #[case(0.0001, TrackStep(4))]
590 fn test_track_step_try_from(#[case] input: f64, #[case] expected_step: TrackStep) {
591 assert_eq!(TrackStep::try_from(input).unwrap(), expected_step);
592 }
593}