diff --git a/src/lib.rs b/src/lib.rs index 8a78917..ab9beb2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ -use std::{path::Display, sync::Arc}; +use std::{num::ParseFloatError, path::Display, str::FromStr, sync::Arc}; -use bevy::utils::{default, HashMap, Instant}; +use bevy::{time::{self, Time}, utils::{default, HashMap}}; use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -8,20 +8,68 @@ pub struct MorphID { pub id: usize, } -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize)] -pub struct RelativeFrameTime(pub f32); - impl MorphID { pub fn new(id: usize) -> Self { Self { - id: id + id } } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct Timestamp { + hour: u8, + minute: u8, + second: u8, + frame: u8, +} + +#[derive(Debug)] +pub enum TimestampParseError { + EmptyTimestamp, + TooManySegments, + NumberParseError(ParseFloatError), +} + +impl std::fmt::Display for TimestampParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("{self:?}")) + } +} + +impl std::error::Error for TimestampParseError {} + +impl FromStr for Timestamp { + type Err = TimestampParseError; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + return Err(TimestampParseError::EmptyTimestamp); + } + + let mut segments = s.split(':').collect::>(); + if segments.len() > 4 { + return Err(TimestampParseError::TooManySegments); + } + + segments.reverse(); + + let Some(frame) = segments.first() else { + 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)?; + + Ok(Self { hour, minute, second, frame }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct CSVFrame { - pub timestamp: RelativeFrameTime, + pub timestamp: Timestamp, pub morphs: HashMap, } @@ -29,13 +77,19 @@ pub struct CSVFrame { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CSVAnimation { pub frames: Vec, - pub morph_names: Vec + pub morph_names: Vec, + pub fps: u8 } #[derive(Debug)] pub enum CSVParseError { NoData, - InvalidCSV(csv::Error) + InvalidCSV(csv::Error), + NoMorphs, + MisssingKnownHeaders, + NoFrames, + TimecodeParseError(TimestampParseError), + MorphParseError(ParseFloatError), } impl std::fmt::Display for CSVParseError { @@ -46,8 +100,7 @@ impl std::fmt::Display for CSVParseError { impl std::error::Error for CSVParseError {} -const TIMECODE_COLUMN_ID : usize = 0; -const MORPH_COLUMNS_START_ID : usize = 2; +const SKIP_COLUMNS_BEFORE_MORPH : usize = 1; impl CSVAnimation { pub fn parse(csv: &str) -> Result { @@ -55,43 +108,57 @@ impl CSVAnimation { return Err(CSVParseError::NoData); } - let mut buffer = csv.as_bytes(); + let buffer = csv.as_bytes(); let mut csv_content = csv::ReaderBuilder::new().has_headers(false).from_reader(buffer); let mut records = csv_content.records(); - let first_row = match records.next() { - Some(Ok(v)) => v, - Some(Err(e)) => { - return Err(CSVParseError::InvalidCSV(e)); - }, - None => { - return Err(CSVParseError::NoData); + let header_row = records.next().ok_or(CSVParseError::NoData)?.map_err(|e| CSVParseError::InvalidCSV(e))?; + + let morph_names : Vec = header_row.iter().enumerate().filter_map(|(i, v)| if i > SKIP_COLUMNS_BEFORE_MORPH { Some(v.trim().to_string()) } else { None }).collect(); + + if morph_names.is_empty() { + return Err(CSVParseError::NoMorphs); + } + + let mut frames = Vec::new(); + let mut fps : u8 = 0; + + + for result in records { + let record = result.map_err(CSVParseError::InvalidCSV)?; + + let mut iter = record.iter(); + + let timestamp = iter.next().ok_or(CSVParseError::MisssingKnownHeaders)?; // This error should be impossible since the CSV parser validates that all rows have the same length, and we already confirmed there is more than 0 characters in the first row. + let timestamp : Timestamp = timestamp.parse().map_err(CSVParseError::TimecodeParseError)?; + for _ in 0..SKIP_COLUMNS_BEFORE_MORPH { + 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 morph_names = first_row.iter().enumerate().filter_map(|(i, v)| if i >= MORPH_COLUMNS_START_ID { Some(v.trim().to_string()) } else { None }).collect(); + let morphs = iter.map(|v| v.parse::().map_err(CSVParseError::MorphParseError)).enumerate().map(|(i, v)| v.map(|v| (MorphID::new(i), v))).collect::,_>>()?; - for result in csv_content.records() { - let record = match result { - Ok(value) => value, - Err(error) => { - return Err(CSVParseError::InvalidCSV(error)); - } - }; + if fps < timestamp.frame { + fps = timestamp.frame; + } + + frames.push(CSVFrame { timestamp, morphs }); } Ok(CSVAnimation { - frames: vec![], + frames, morph_names, + fps }) } } #[cfg(test)] mod tests { - use crate::{CSVAnimation, CSVParseError}; + use bevy::time::{self, Time}; + + use crate::{CSVAnimation, CSVParseError, Timestamp, TimestampParseError}; #[test] fn given_no_data_it_emits_a_no_data_error() { @@ -100,15 +167,57 @@ mod tests { #[test] fn given_an_invalid_csv_it_emits_an_invalid_csv_error() { - assert!(matches!(CSVAnimation::parse("an invalid csv file + let result = CSVAnimation::parse("an, invalid, csv, file seems to be here... right now? - I wonder, will it be caught"), Err(CSVParseError::InvalidCSV(_)))) + I wonder, will it be caught"); + if !matches!(result, Err(CSVParseError::InvalidCSV(_))) { + panic!( "got {result:?} instead of a CSVParseError"); + } } #[test] fn given_a_header_row_it_sets_correct_headers() { - let result = CSVAnimation::parse("timecode, blendshapecount, shape_1, shape_2").unwrap(); - assert_eq!(result.morph_names.get(0).unwrap(), "shape_1"); + let result = CSVAnimation::parse("timecode, blendshapecount, shape_1, shape_2 + 0, 0, 0, 0").unwrap(); + assert_eq!(result.morph_names.first().unwrap(), "shape_1"); assert_eq!(result.morph_names.get(1).unwrap(), "shape_2"); } + + #[test] + fn given_no_morph_headers_it_emits_a_no_morph_error() { + let result = CSVAnimation::parse("timecode, blendshapecount").unwrap_err(); + assert!(matches!(result, CSVParseError::NoMorphs)); + } + + #[test] + fn given_no_frames_it_emits_a_no_frame_error() { + let result = CSVAnimation::parse("timecode, blendshapecount, shape_1, shape_2").unwrap_err(); + assert!(matches!(result, CSVParseError::NoFrames)); + } + + + #[test] + fn given_a_single_frame_it_has_a_time_of_0() { + let result = CSVAnimation::parse("timecode, blendshapecount, shape_1, shape_2 +00:00:00:00.00, 0, 0, 0").unwrap(); + assert_eq!(result.frames.first().unwrap().timestamp.frame, 0); + } + + #[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_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 }) + } }