diff --git a/examples/simple_example.rs b/examples/simple_example.rs index 6d8824c..9537bea 100644 --- a/examples/simple_example.rs +++ b/examples/simple_example.rs @@ -1,4 +1,7 @@ -use bevy::{pbr::{CascadeShadowConfigBuilder, DirectionalLightShadowMap}, prelude::*}; +use bevy::{ + pbr::{CascadeShadowConfigBuilder, DirectionalLightShadowMap}, + prelude::*, +}; use bevy_inspector_egui::quick::WorldInspectorPlugin; fn main() { @@ -11,14 +14,11 @@ fn main() { } fn setup(mut commands: Commands, asset_server: Res) { - commands.spawn( - Camera3dBundle { - transform: Transform::from_xyz(0.7, 0.7, 1.0) - .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), - ..default() - } - ); - + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + ..default() + }); + commands.spawn(DirectionalLightBundle { directional_light: DirectionalLight { shadows_enabled: true, @@ -37,9 +37,8 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }); - commands.spawn(SceneBundle { scene: asset_server.load("example.gltf#Scene0"), ..default() }); -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index aab6ba0..2f8c68d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ -use std::{num::ParseFloatError, path::Display, str::FromStr, sync::Arc}; +use std::{num::ParseFloatError, str::FromStr}; + -use bevy::{time::{self, Time}, utils::{default, HashMap}}; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] @@ -46,27 +46,54 @@ impl FromStr for Timestamp { return Err(TimestampParseError::EmptyTimestamp); }; - 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}")))?; + 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 }) + Ok(Self { + hour, + minute, + second, + frame, + }) } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct CSVFrame { pub timestamp: Timestamp, - pub morphs: Vec + pub morphs: Vec, } - #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CSVAnimation { pub frames: Vec, pub morph_names: Vec, - pub fps: u8 + pub fps: u8, } #[derive(Debug)] @@ -88,31 +115,45 @@ impl std::fmt::Display for CSVParseError { impl std::error::Error for CSVParseError {} -const SKIP_COLUMNS_BEFORE_MORPH : usize = 1; +const SKIP_COLUMNS_BEFORE_MORPH: usize = 1; impl CSVAnimation { - pub fn parse(csv: &str) -> Result { + pub fn parse(csv: &str) -> Result { if csv.is_empty() { return Err(CSVParseError::NoData); } let buffer = csv.as_bytes(); - let mut csv_content = csv::ReaderBuilder::new().has_headers(false).from_reader(buffer); - + let mut csv_content = csv::ReaderBuilder::new() + .has_headers(false) + .from_reader(buffer); + let mut records = csv_content.records(); - let header_row = records.next().ok_or(CSVParseError::NoData)?.map_err(|e| CSVParseError::InvalidCSV(e))?; + let header_row = records + .next() + .ok_or(CSVParseError::NoData)? + .map_err(CSVParseError::InvalidCSV)?; - 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(); + 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; - + let mut fps: u8 = 0; for result in records { let record = result.map_err(CSVParseError::InvalidCSV)?; @@ -120,12 +161,21 @@ impl CSVAnimation { 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)?; + 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 _ = 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.trim().parse::().map_err(CSVParseError::MorphParseError)).collect::,_>>()?; + let morphs = iter + .map(|v| { + v.trim() + .parse::() + .map_err(CSVParseError::MorphParseError) + }) + .collect::, _>>()?; if fps < timestamp.frame { fps = timestamp.frame; @@ -141,59 +191,91 @@ impl CSVAnimation { Ok(CSVAnimation { frames, morph_names, - fps: fps + 1 + fps: fps + 1, }) } } #[cfg(test)] 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"); + 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"); + 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 }) + 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 }) + 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() { - assert!(matches!(CSVAnimation::parse("").err().unwrap(), CSVParseError::NoData)) + assert!(matches!( + CSVAnimation::parse("").err().unwrap(), + CSVParseError::NoData + )) } #[test] fn given_an_invalid_csv_it_emits_an_invalid_csv_error() { - let result = 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"); + I wonder, will it be caught", + ); if !matches!(result, Err(CSVParseError::InvalidCSV(_))) { - panic!( "got {result:?} instead of a CSVParseError"); + 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 - 00:00:00:00, 0, 0, 0").unwrap(); + let result = CSVAnimation::parse( + "timecode, blendshapecount, shape_1, shape_2 + 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"); } @@ -206,33 +288,42 @@ mod tests { #[test] fn given_no_frames_it_emits_a_no_frame_error() { - let result = CSVAnimation::parse("timecode, blendshapecount, shape_1, shape_2").unwrap_err(); + 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(); + 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_multiple_frames_can_deduce_fps() { - let result = CSVAnimation::parse("timecode, blendshapecount, shape_1, shape_2 + 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(); +00:00:01:01.00, 0, 0, 0", + ) + .unwrap(); assert_eq!(result.fps, 3); } #[test] 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(); + 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); }