mirror of
https://github.com/storytold/bevy-mocap.git
synced 2026-10-21 07:45:06 +00:00
frames parse correctly
This commit is contained in:
+50
-34
@@ -3,19 +3,6 @@ use std::{num::ParseFloatError, path::Display, str::FromStr, sync::Arc};
|
|||||||
use bevy::{time::{self, Time}, utils::{default, HashMap}};
|
use bevy::{time::{self, Time}, utils::{default, HashMap}};
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct Timestamp {
|
pub struct Timestamp {
|
||||||
hour: u8,
|
hour: u8,
|
||||||
@@ -28,7 +15,7 @@ pub struct Timestamp {
|
|||||||
pub enum TimestampParseError {
|
pub enum TimestampParseError {
|
||||||
EmptyTimestamp,
|
EmptyTimestamp,
|
||||||
TooManySegments,
|
TooManySegments,
|
||||||
NumberParseError(ParseFloatError),
|
NumberParseError(ParseFloatError, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for TimestampParseError {
|
impl std::fmt::Display for TimestampParseError {
|
||||||
@@ -43,6 +30,7 @@ impl FromStr for Timestamp {
|
|||||||
type Err = TimestampParseError;
|
type Err = TimestampParseError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let s = s.trim();
|
||||||
if s.is_empty() {
|
if s.is_empty() {
|
||||||
return Err(TimestampParseError::EmptyTimestamp);
|
return Err(TimestampParseError::EmptyTimestamp);
|
||||||
}
|
}
|
||||||
@@ -58,10 +46,10 @@ impl FromStr for Timestamp {
|
|||||||
return Err(TimestampParseError::EmptyTimestamp);
|
return Err(TimestampParseError::EmptyTimestamp);
|
||||||
};
|
};
|
||||||
|
|
||||||
let frame = frame.parse::<f32>().map(|v| v.floor() as u8).map_err(TimestampParseError::NumberParseError)?;
|
let frame = frame.parse::<f32>().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::<f32>().map(|v| v.floor() as u8).map_err(TimestampParseError::NumberParseError)?;
|
let second = segments.get(1).unwrap_or(&"0").parse::<f32>().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::<f32>().map(|v| v.floor() as u8).map_err(TimestampParseError::NumberParseError)?;
|
let minute = segments.get(2).unwrap_or(&"0").parse::<f32>().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::<f32>().map(|v| v.floor() as u8).map_err(TimestampParseError::NumberParseError)?;
|
let hour = segments.get(3).unwrap_or(&"0").parse::<f32>().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 })
|
||||||
}
|
}
|
||||||
@@ -70,7 +58,7 @@ impl FromStr for Timestamp {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct CSVFrame {
|
pub struct CSVFrame {
|
||||||
pub timestamp: Timestamp,
|
pub timestamp: Timestamp,
|
||||||
pub morphs: HashMap<MorphID, f32>,
|
pub morphs: Vec<f32>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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 _ = 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::<f32>().map_err(CSVParseError::MorphParseError)).enumerate().map(|(i, v)| v.map(|v| (MorphID::new(i), v))).collect::<Result<HashMap<_, _>,_>>()?;
|
let morphs = iter.map(|v| v.trim().parse::<f32>().map_err(CSVParseError::MorphParseError)).collect::<Result<Vec<_>,_>>()?;
|
||||||
|
|
||||||
if fps < timestamp.frame {
|
if fps < timestamp.frame {
|
||||||
fps = timestamp.frame;
|
fps = timestamp.frame;
|
||||||
@@ -146,10 +134,14 @@ impl CSVAnimation {
|
|||||||
frames.push(CSVFrame { timestamp, morphs });
|
frames.push(CSVFrame { timestamp, morphs });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if frames.is_empty() {
|
||||||
|
return Err(CSVParseError::NoFrames);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(CSVAnimation {
|
Ok(CSVAnimation {
|
||||||
frames,
|
frames,
|
||||||
morph_names,
|
morph_names,
|
||||||
fps
|
fps: fps + 1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,6 +151,29 @@ mod tests {
|
|||||||
use bevy::time::{self, Time};
|
use bevy::time::{self, Time};
|
||||||
|
|
||||||
use crate::{CSVAnimation, CSVParseError, Timestamp, TimestampParseError};
|
use crate::{CSVAnimation, CSVParseError, Timestamp, TimestampParseError};
|
||||||
|
#[test]
|
||||||
|
fn given_an_empty_timestamp_it_returns_an_empty_timestamp_error() {
|
||||||
|
let timestamp = "".parse::<Timestamp>().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::<Timestamp>().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::<Timestamp>().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::<Timestamp>().expect("Timestamp failed to parse");
|
||||||
|
assert_eq!(timestamp, Timestamp { hour: 1, minute: 2, second: 3, frame: 4 })
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn given_no_data_it_emits_a_no_data_error() {
|
fn given_no_data_it_emits_a_no_data_error() {
|
||||||
@@ -178,7 +193,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn given_a_header_row_it_sets_correct_headers() {
|
fn given_a_header_row_it_sets_correct_headers() {
|
||||||
let result = CSVAnimation::parse("timecode, blendshapecount, shape_1, shape_2
|
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.first().unwrap(), "shape_1");
|
||||||
assert_eq!(result.morph_names.get(1).unwrap(), "shape_2");
|
assert_eq!(result.morph_names.get(1).unwrap(), "shape_2");
|
||||||
}
|
}
|
||||||
@@ -204,20 +219,21 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn given_an_empty_timestamp_it_returns_an_empty_timestamp_error() {
|
fn given_multiple_frames_can_deduce_fps() {
|
||||||
let timestamp = "".parse::<Timestamp>().expect_err("succeeded against expectations");
|
let result = CSVAnimation::parse("timecode, blendshapecount, shape_1, shape_2
|
||||||
assert!(matches!(timestamp, TimestampParseError::EmptyTimestamp))
|
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]
|
#[test]
|
||||||
fn given_a_timestamp_with_more_than_4_segments_returns_a_too_many_segments_error() {
|
fn given_a_single_frame_it_parses_morphs_correctly() {
|
||||||
let timestamp = "00:00:00:00:00.1234".parse::<Timestamp>().expect_err("succeeded against expectations");
|
let result = CSVAnimation::parse("timecode, blendshapecount, shape_1, shape_2
|
||||||
assert!(matches!(timestamp, TimestampParseError::TooManySegments))
|
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);
|
||||||
#[test]
|
|
||||||
fn given_a_valid_timestamp_it_is_parsed_correctly() {
|
|
||||||
let timestamp = "01:02:03:04.123".parse::<Timestamp>().expect("Timestamp failed to parse");
|
|
||||||
assert_eq!(timestamp, Timestamp { hour: 1, minute: 2, second: 3, frame: 4 })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user