mirror of
https://github.com/storytold/bevy-mocap.git
synced 2026-10-13 23:52:47 +00:00
parse timestamp
This commit is contained in:
+144
-35
@@ -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<Self, Self::Err> {
|
||||
if s.is_empty() {
|
||||
return Err(TimestampParseError::EmptyTimestamp);
|
||||
}
|
||||
|
||||
let mut segments = s.split(':').collect::<Vec<_>>();
|
||||
if segments.len() > 4 {
|
||||
return Err(TimestampParseError::TooManySegments);
|
||||
}
|
||||
|
||||
segments.reverse();
|
||||
|
||||
let Some(frame) = segments.first() else {
|
||||
return Err(TimestampParseError::EmptyTimestamp);
|
||||
};
|
||||
|
||||
let frame = frame.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(TimestampParseError::NumberParseError)?;
|
||||
let minute = segments.get(2).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(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<MorphID, f32>,
|
||||
}
|
||||
|
||||
@@ -29,13 +77,19 @@ pub struct CSVFrame {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CSVAnimation {
|
||||
pub frames: Vec<CSVFrame>,
|
||||
pub morph_names: Vec<String>
|
||||
pub morph_names: Vec<String>,
|
||||
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<CSVAnimation, CSVParseError> {
|
||||
@@ -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<String> = 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::<f32>().map_err(CSVParseError::MorphParseError)).enumerate().map(|(i, v)| v.map(|v| (MorphID::new(i), v))).collect::<Result<HashMap<_, _>,_>>()?;
|
||||
|
||||
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::<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_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