use bunny_mesh::Triangle32;
mod error;
mod float;
mod iter;
pub use error::ObjError;
use iter::find_record;
pub use iter::{ObjTriangles, ObjVertices};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ObjMesh<'a> {
source: &'a str,
vertex_count: usize,
face_count: usize,
}
impl<'a> ObjMesh<'a> {
#[must_use]
pub const fn source(self) -> &'a str {
self.source
}
#[must_use]
pub const fn vertex_count(self) -> usize {
self.vertex_count
}
#[must_use]
pub const fn face_count(self) -> usize {
self.face_count
}
pub fn vertex(self, index: usize) -> Result<ObjVertex, ObjError> {
find_record(self.source, "v", index)
.ok_or(ObjError::IndexOutOfBounds)
.and_then(parse_vertex_line)
}
pub fn triangle(self, index: usize) -> Result<Triangle32, ObjError> {
find_record(self.source, "f", index)
.ok_or(ObjError::IndexOutOfBounds)
.and_then(parse_face_line)
}
#[must_use]
pub fn vertices(self) -> ObjVertices<'a> {
ObjVertices::new(self.source)
}
#[must_use]
pub fn triangles(self) -> ObjTriangles<'a> {
ObjTriangles::new(self.source, self.vertex_count)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ObjVertex {
pub x: f32,
pub y: f32,
pub z: f32,
}
pub fn parse_obj_text(source: &str) -> Result<ObjMesh<'_>, ObjError> {
let counts = count_records(source)?;
if counts.vertex_count == 0 {
return Err(ObjError::MissingVertices);
}
if counts.face_count == 0 {
return Err(ObjError::MissingFaces);
}
validate_face_indices(source, counts.vertex_count)?;
Ok(ObjMesh { source, vertex_count: counts.vertex_count, face_count: counts.face_count })
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct ObjCounts {
vertex_count: usize,
face_count: usize,
}
fn count_records(source: &str) -> Result<ObjCounts, ObjError> {
let mut vertex_count = 0;
let mut face_count = 0;
for line in source.lines() {
match statement_kind(line)? {
Some("v") => {
parse_vertex_line(line)?;
vertex_count += 1;
}
Some("f") => {
parse_face_line(line)?;
face_count += 1;
}
Some(_) | None => {}
}
}
Ok(ObjCounts { vertex_count, face_count })
}
fn validate_face_indices(source: &str, vertex_count: usize) -> Result<(), ObjError> {
for line in source.lines() {
if statement_kind(line)? == Some("f") {
let triangle = parse_face_line(line)?;
if !face_indices_are_valid(triangle, vertex_count) {
return Err(ObjError::IndexOutOfBounds);
}
}
}
Ok(())
}
fn face_indices_are_valid(face: Triangle32, vertex_count: usize) -> bool {
u32_index_is_valid(face.v0, vertex_count)
&& u32_index_is_valid(face.v1, vertex_count)
&& u32_index_is_valid(face.v2, vertex_count)
}
fn u32_index_is_valid(index: u32, vertex_count: usize) -> bool {
usize::try_from(index).is_ok_and(|index| index < vertex_count)
}
fn statement_kind(line: &str) -> Result<Option<&str>, ObjError> {
let mut parts = record_body(line).split_whitespace();
let Some(kind) = parts.next() else {
return Ok(None);
};
if kind.starts_with('#') || harmless_statement(kind) {
Ok(None)
} else if matches!(kind, "v" | "f") {
Ok(Some(kind))
} else {
Err(ObjError::UnsupportedStatement)
}
}
fn harmless_statement(kind: &str) -> bool {
matches!(kind, "o" | "g" | "s" | "usemtl" | "mtllib" | "vt" | "vn")
}
fn record_body(line: &str) -> &str {
line.split_once('#').map_or(line, |(record, _comment)| record).trim()
}
fn parse_vertex_line(line: &str) -> Result<ObjVertex, ObjError> {
let mut parts = record_body(line).split_whitespace();
if parts.next() != Some("v") {
return Err(ObjError::InvalidVertex);
}
let vertex = ObjVertex {
x: parse_coord(parts.next())?,
y: parse_coord(parts.next())?,
z: parse_coord(parts.next())?,
};
if !(vertex.x.is_finite() && vertex.y.is_finite() && vertex.z.is_finite()) {
return Err(ObjError::NonFiniteVertex);
}
if parts.next().is_some() {
Err(ObjError::InvalidVertex)
} else {
Ok(vertex)
}
}
fn parse_coord(value: Option<&str>) -> Result<f32, ObjError> {
float::parse_ascii_float(value.ok_or(ObjError::InvalidVertex)?).ok_or(ObjError::InvalidVertex)
}
fn parse_face_line(line: &str) -> Result<Triangle32, ObjError> {
let mut parts = record_body(line).split_whitespace();
if parts.next() != Some("f") {
return Err(ObjError::NonTriangularFace);
}
let face = Triangle32::new(
parse_index(parts.next())?,
parse_index(parts.next())?,
parse_index(parts.next())?,
);
if parts.next().is_some() {
Err(ObjError::NonTriangularFace)
} else {
Ok(face)
}
}
fn parse_index(token: Option<&str>) -> Result<u32, ObjError> {
let token = token.ok_or(ObjError::NonTriangularFace)?;
let mut fields = token.split('/');
let vertex_index = parse_vertex_index(fields.next().ok_or(ObjError::InvalidIndex)?)?;
match (fields.next(), fields.next(), fields.next()) {
(None, None, None) => Ok(vertex_index),
(Some(texture), None, None) => {
parse_auxiliary_index(texture)?;
Ok(vertex_index)
}
(Some(texture), Some(normal), None) => {
if !texture.is_empty() {
parse_auxiliary_index(texture)?;
}
parse_auxiliary_index(normal)?;
Ok(vertex_index)
}
_ => Err(ObjError::InvalidIndex),
}
}
fn parse_vertex_index(index_text: &str) -> Result<u32, ObjError> {
let one_based = index_text.parse::<i64>().map_err(|_| ObjError::InvalidIndex)?;
let zero_based = one_based.checked_sub(1).ok_or(ObjError::InvalidIndex)?;
u32::try_from(zero_based).map_err(|_| ObjError::InvalidIndex)
}
fn parse_auxiliary_index(index_text: &str) -> Result<(), ObjError> {
let one_based = index_text.parse::<i64>().map_err(|_| ObjError::InvalidIndex)?;
let zero_based = one_based.checked_sub(1).ok_or(ObjError::InvalidIndex)?;
u32::try_from(zero_based).map(|_| ()).map_err(|_| ObjError::InvalidIndex)
}