pub(crate) struct MniMeshRaw {
pub point_array: Vec<f32>,
pub indices: Vec<u32>,
}
#[derive(thiserror::Error, Debug, PartialEq)]
pub(crate) enum MniMeshParseError {
#[error("Unsupported mesh feature: {0}")]
Unsupported(String),
#[error("File is empty")]
EmptyFile,
#[error("Missing SurfProp")]
MissingSurfProp,
#[error("Cannot read n_points")]
CannotReadNPoints,
#[error("Number of points found {n_found} is fewer than n_points={n_points}")]
TooFewPoints { n_points: usize, n_found: usize },
#[error("Point data at index {0} is not a float")]
InvalidPoint(usize),
#[error("Cannot parse nitems (number of polygons appearing after the normal vectors)")]
InvalidNItems,
#[error("Missing colour flag")]
MissingColourFlag,
#[error("Missing end indices")]
MissingEndIndices { nitems: usize, n_found: usize },
#[error("First element of end indices must be 3")]
InvalidFirstEndIndex,
#[error("Invalid end index at {0} (end indices array must be a sequence of natural numbers counting up by 3, i.e. only triangular meshes are supported)")]
InvalidEndIndices(usize),
#[error("Member of indices array at {0} cannot be parsed as float")]
InvalidIndex(usize),
#[error("Expected indices array to have length {expected} (greatest value in end_indices) however found {actual} values")]
InvalidIndicesLength { expected: usize, actual: usize },
#[error("Colour table is shorter than expected: expected {expected} elements, found {actual}")]
MissingColourTable { expected: usize, actual: usize },
#[error("Expected to find {n_vectors} normal vectors, but only found {n_found}")]
TooFewNormals { n_vectors: usize, n_found: usize },
}
impl MniMeshRaw {
pub fn parse_obj<S: AsRef<str>>(
mut data: impl Iterator<Item = S>,
) -> Result<Self, MniMeshParseError> {
consume_header(&mut data)?;
let n_points: usize = data
.next()
.and_then(|s| s.as_ref().parse().ok())
.ok_or(MniMeshParseError::CannotReadNPoints)?;
let point_array = (0..(n_points * 3))
.map(|i| {
data.next()
.ok_or(MniMeshParseError::TooFewPoints {
n_points,
n_found: i + 1,
})
.and_then(|s| {
s.as_ref()
.parse()
.map_err(|_| MniMeshParseError::InvalidPoint(i))
})
})
.collect::<Result<_, MniMeshParseError>>()?;
let n_normal_vector_components = (0..(n_points * 3))
.map(|_| data.next())
.filter(|e| e.is_some())
.count();
if n_normal_vector_components != (n_points * 3) {
let n_vectors = n_points / 3;
let n_found = n_normal_vector_components / 3;
return Err(MniMeshParseError::TooFewNormals { n_vectors, n_found });
}
let nitems: usize = data
.next()
.ok_or(MniMeshParseError::InvalidNItems)
.and_then(|s| {
s.as_ref()
.parse()
.map_err(|_| MniMeshParseError::InvalidNItems)
})?;
let ele = data.next().ok_or(MniMeshParseError::MissingColourFlag)?;
let colour_flag = ele.as_ref();
if colour_flag != "0" {
let msg =
format!("Unsupported colour flag: {colour_flag} (only the value 0 is supported)");
return Err(MniMeshParseError::Unsupported(msg));
}
let colour_table_length = (0..4).map(|_| data.next()).filter(|o| o.is_some()).count();
if colour_table_length != 4 {
return Err(MniMeshParseError::MissingColourTable {
expected: 4,
actual: colour_table_length,
});
}
let last_end_index = consume_end_indices(&mut data, nitems)?;
let indices = data
.map(|s| s.as_ref().parse())
.enumerate()
.map(|(i, r)| r.map_err(|_| MniMeshParseError::InvalidIndex(i)))
.collect::<Result<Vec<_>, MniMeshParseError>>()?;
if indices.len() != last_end_index {
return Err(MniMeshParseError::InvalidIndicesLength {
expected: last_end_index,
actual: indices.len(),
});
}
let ret = Self {
point_array,
indices,
};
Ok(ret)
}
}
fn consume_header<S: AsRef<str>>(
data: &mut impl Iterator<Item = S>,
) -> Result<(), MniMeshParseError> {
let element = data.next().ok_or(MniMeshParseError::EmptyFile)?;
let object_class = element.as_ref();
if object_class != "P" {
let msg =
format!("Unsupported object class: {object_class} (First token of file must be \"P\")");
return Err(MniMeshParseError::Unsupported(msg));
}
let has_surfprop = (0..5).map(|_| data.next()).all(|o| o.is_some());
if has_surfprop {
Ok(())
} else {
Err(MniMeshParseError::MissingSurfProp)
}
}
fn consume_end_indices<S: AsRef<str>>(
data: &mut impl Iterator<Item = S>,
nitems: usize,
) -> Result<usize, MniMeshParseError> {
let first_end_index = data
.next()
.ok_or(MniMeshParseError::MissingEndIndices { nitems, n_found: 0 })?;
if first_end_index.as_ref() != "3" {
return Err(MniMeshParseError::InvalidFirstEndIndex);
}
(0..(nitems - 1))
.map(|i| {
let i = i + 1;
data.next()
.ok_or(MniMeshParseError::MissingEndIndices { nitems, n_found: i })
.map(|s| (i, s))
})
.try_fold((0, 3), |(_prev_i, prev), next_result| {
next_result.and_then(|(i, next_ele)| {
if let Ok(next) = next_ele.as_ref().parse() {
if prev + 3 == next {
Ok((i, next))
} else {
Err(MniMeshParseError::InvalidEndIndices(i))
}
} else {
Err(MniMeshParseError::InvalidEndIndices(i))
}
})
})
.map(|(_i, last)| last)
}
#[cfg(test)]
mod test {
use super::*;
use rstest::*;
#[rstest]
fn test_consume_end_indices_works() {
let input = &["3", "6", "9", "12", "leftover0", "leftover1"];
let nitems = 4;
let mut data = input.iter();
assert_eq!(consume_end_indices(&mut data, nitems), Ok(12));
assert_eq!(data.count(), 2);
}
#[rstest]
fn test_consume_end_indices_bad_start() {
let input = &["2", "6", "9", "12", "leftover0", "leftover1"];
let nitems = 4;
let mut data = input.iter();
assert_eq!(
consume_end_indices(&mut data, nitems),
Err(MniMeshParseError::InvalidFirstEndIndex)
)
}
#[rstest]
#[case(&["3", "5", "9", "12", "leftover0", "leftover1"], 4, 1)]
#[case(&["3", "6", "8", "12", "leftover0", "leftover1"], 4, 2)]
#[case(&["3", "6", "10", "12", "leftover0", "leftover1"], 4, 2)]
fn test_consume_end_indices_invalid_at(
#[case] input: &[&'static str],
#[case] nitems: usize,
#[case] invalid_index: usize,
) {
let mut data = input.iter();
assert_eq!(
consume_end_indices(&mut data, nitems),
Err(MniMeshParseError::InvalidEndIndices(invalid_index))
)
}
}