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