diff --git a/src/lib.rs b/src/lib.rs index ab9beb2..aab6ba0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,19 +3,6 @@ use std::{num::ParseFloatError, path::Display, str::FromStr, sync::Arc}; use bevy::{time::{self, Time}, utils::{default, HashMap}}; use serde::{Deserialize, Serialize}; -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct MorphID { - pub id: usize, -} - -impl MorphID { - pub fn new(id: usize) -> Self { - Self { - id - } - } -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct Timestamp { hour: u8, @@ -28,7 +15,7 @@ pub struct Timestamp { pub enum TimestampParseError { EmptyTimestamp, TooManySegments, - NumberParseError(ParseFloatError), + NumberParseError(ParseFloatError, String), } impl std::fmt::Display for TimestampParseError { @@ -43,6 +30,7 @@ impl FromStr for Timestamp { type Err = TimestampParseError; fn from_str(s: &str) -> Result { + let s = s.trim(); if s.is_empty() { return Err(TimestampParseError::EmptyTimestamp); } @@ -58,10 +46,10 @@ impl FromStr for Timestamp { return Err(TimestampParseError::EmptyTimestamp); }; - let frame = frame.parse::().map(|v| v.floor() as u8).map_err(TimestampParseError::NumberParseError)?; - let second = segments.get(1).unwrap_or(&"0").parse::().map(|v| v.floor() as u8).map_err(TimestampParseError::NumberParseError)?; - let minute = segments.get(2).unwrap_or(&"0").parse::().map(|v| v.floor() as u8).map_err(TimestampParseError::NumberParseError)?; - let hour = segments.get(3).unwrap_or(&"0").parse::().map(|v| v.floor() as u8).map_err(TimestampParseError::NumberParseError)?; + let frame = frame.parse::().map(|v| v.floor() as u8).map_err(|e| TimestampParseError::NumberParseError(e, format!("frame failed for {s}")))?; + let second = segments.get(1).unwrap_or(&"0").parse::().map(|v| v.floor() as u8).map_err(|e| TimestampParseError::NumberParseError(e, format!("second failed for {s}")))?; + let minute = segments.get(2).unwrap_or(&"0").parse::().map(|v| v.floor() as u8).map_err(|e| TimestampParseError::NumberParseError(e, format!("minute failed for {s}")))?; + let hour = segments.get(3).unwrap_or(&"0").parse::().map(|v| v.floor() as u8).map_err(|e| TimestampParseError::NumberParseError(e, format!("hour failed for {s}")))?; Ok(Self { hour, minute, second, frame }) } @@ -70,7 +58,7 @@ impl FromStr for Timestamp { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct CSVFrame { pub timestamp: Timestamp, - pub morphs: HashMap, + pub morphs: Vec } @@ -137,7 +125,7 @@ impl CSVAnimation { let _ = iter.next().ok_or(CSVParseError::MisssingKnownHeaders)?; // similarly, we already validate that there are a single morph in the CSV, so the skipped columns shouldn't be valid CSVs. } - let morphs = iter.map(|v| v.parse::().map_err(CSVParseError::MorphParseError)).enumerate().map(|(i, v)| v.map(|v| (MorphID::new(i), v))).collect::,_>>()?; + let morphs = iter.map(|v| v.trim().parse::().map_err(CSVParseError::MorphParseError)).collect::,_>>()?; if fps < timestamp.frame { fps = timestamp.frame; @@ -146,10 +134,14 @@ impl CSVAnimation { frames.push(CSVFrame { timestamp, morphs }); } + if frames.is_empty() { + return Err(CSVParseError::NoFrames); + } + Ok(CSVAnimation { frames, morph_names, - fps + fps: fps + 1 }) } } @@ -159,6 +151,29 @@ mod tests { use bevy::time::{self, Time}; use crate::{CSVAnimation, CSVParseError, Timestamp, TimestampParseError}; + #[test] + fn given_an_empty_timestamp_it_returns_an_empty_timestamp_error() { + let timestamp = "".parse::().expect_err("succeeded against expectations"); + assert!(matches!(timestamp, TimestampParseError::EmptyTimestamp)) + } + + #[test] + fn given_a_timestamp_with_more_than_4_segments_returns_a_too_many_segments_error() { + let timestamp = "00:00:00:00:00.1234".parse::().expect_err("succeeded against expectations"); + assert!(matches!(timestamp, TimestampParseError::TooManySegments)) + } + + #[test] + fn given_a_zero_timestamp_it_is_parsed_correctly() { + let timestamp = "00:00:00:00".parse::().expect("Timestamp failed to parse"); + assert_eq!(timestamp, Timestamp { hour: 0, minute: 0, second: 0, frame: 0 }) + } + + #[test] + fn given_a_valid_timestamp_it_is_parsed_correctly() { + let timestamp = "01:02:03:04.123".parse::().expect("Timestamp failed to parse"); + assert_eq!(timestamp, Timestamp { hour: 1, minute: 2, second: 3, frame: 4 }) + } #[test] fn given_no_data_it_emits_a_no_data_error() { @@ -178,7 +193,7 @@ mod tests { #[test] fn given_a_header_row_it_sets_correct_headers() { let result = CSVAnimation::parse("timecode, blendshapecount, shape_1, shape_2 - 0, 0, 0, 0").unwrap(); + 00:00:00:00, 0, 0, 0").unwrap(); assert_eq!(result.morph_names.first().unwrap(), "shape_1"); assert_eq!(result.morph_names.get(1).unwrap(), "shape_2"); } @@ -204,20 +219,21 @@ mod tests { } #[test] - fn given_an_empty_timestamp_it_returns_an_empty_timestamp_error() { - let timestamp = "".parse::().expect_err("succeeded against expectations"); - assert!(matches!(timestamp, TimestampParseError::EmptyTimestamp)) + fn given_multiple_frames_can_deduce_fps() { + let result = CSVAnimation::parse("timecode, blendshapecount, shape_1, shape_2 +00:00:00:00.00, 0, 0, 0 +00:00:00:01.00, 0, 0, 0 +00:00:00:02.00, 0, 0, 0 +00:00:01:00.00, 0, 0, 0 +00:00:01:01.00, 0, 0, 0").unwrap(); + assert_eq!(result.fps, 3); } #[test] - fn given_a_timestamp_with_more_than_4_segments_returns_a_too_many_segments_error() { - let timestamp = "00:00:00:00:00.1234".parse::().expect_err("succeeded against expectations"); - assert!(matches!(timestamp, TimestampParseError::TooManySegments)) - } - - #[test] - fn given_a_valid_timestamp_it_is_parsed_correctly() { - let timestamp = "01:02:03:04.123".parse::().expect("Timestamp failed to parse"); - assert_eq!(timestamp, Timestamp { hour: 1, minute: 2, second: 3, frame: 4 }) + fn given_a_single_frame_it_parses_morphs_correctly() { + let result = CSVAnimation::parse("timecode, blendshapecount, shape_1, shape_2 +00:00:00:00.00, 0, 0.1, 0.5").unwrap(); + assert!(&result.frames.first().unwrap().morphs[0] - 0.1 < 0.0001); + assert!(&result.frames.first().unwrap().morphs[1] - 0.5 < 0.0001); } }