use crate::error::{DagError, DagResult};
use crate::types::IndexedGeometry;
pub fn parse_obj(text: &str) -> DagResult<IndexedGeometry> {
let mut positions: Vec<f32> = Vec::new();
let mut raw_faces: Vec<[usize; 3]> = Vec::new();
for (line_no, raw_line) in text.lines().enumerate() {
let line_no = line_no + 1;
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut tokens = line.split_whitespace();
let tag = tokens.next().unwrap_or_default();
match tag {
"v" => {
let coords: Vec<f32> = tokens
.map(|t| parse_f32(t, line_no))
.collect::<DagResult<_>>()?;
if coords.len() < 3 {
return Err(DagError::ObjParse {
line: line_no,
reason: format!("vertex needs 3 (optionally 4 with w) components, got {}", coords.len()),
});
}
if !coords[..3].iter().all(|v| v.is_finite()) {
return Err(DagError::ObjParse { line: line_no, reason: "vertex component must be finite".into() });
}
positions.extend_from_slice(&coords[..3]);
}
"f" => {
let indices: Vec<usize> = tokens
.map(|t| parse_face_vertex(t, line_no, positions.len() / 3))
.collect::<DagResult<_>>()?;
if indices.len() != 3 {
return Err(DagError::ObjParse {
line: line_no,
reason: format!("only triangle faces are supported, got {} vertices", indices.len()),
});
}
raw_faces.push([indices[0], indices[1], indices[2]]);
}
_ => {}
}
}
let mut indices = Vec::with_capacity(raw_faces.len() * 3);
for (face_no, face) in raw_faces.iter().enumerate() {
for &vertex in face {
if vertex >= positions.len() / 3 {
return Err(DagError::ObjParse {
line: 0,
reason: format!("face {} references vertex {} beyond {} defined", face_no + 1, vertex + 1, positions.len() / 3),
});
}
indices.push(vertex as u32);
}
}
Ok(IndexedGeometry { positions, indices })
}
fn parse_f32(token: &str, line_no: usize) -> DagResult<f32> {
token
.parse::<f32>()
.map_err(|_| DagError::ObjParse { line: line_no, reason: format!("invalid number: {token:?}") })
}
fn parse_face_vertex(token: &str, line_no: usize, vertex_count: usize) -> DagResult<usize> {
let vertex_str = token.split('/').next().unwrap_or_default();
let raw: i64 = vertex_str
.parse::<i64>()
.map_err(|_| DagError::ObjParse { line: line_no, reason: format!("invalid face index: {token:?}") })?;
if raw == 0 {
return Err(DagError::ObjParse { line: line_no, reason: "face index 0 is not valid OBJ".into() });
}
if raw > 0 {
Ok((raw - 1) as usize)
} else {
if vertex_count == 0 {
return Err(DagError::ObjParse { line: line_no, reason: "negative face index before any vertex".into() });
}
let resolved = vertex_count as i64 + raw;
Ok(resolved as usize)
}
}
#[must_use]
pub fn write_obj(geometry: &IndexedGeometry, comment: &str) -> String {
let mut out = String::with_capacity(geometry.positions.len() * 16 + geometry.indices.len() * 16);
out.push_str("# ");
out.push_str(comment);
out.push('\n');
for tri in geometry.positions.chunks_exact(3) {
out.push_str(&format!("v {} {} {}\n", tri[0], tri[1], tri[2]));
}
for face in geometry.indices.chunks_exact(3) {
out.push_str(&format!("f {} {} {}\n", face[0] + 1, face[1] + 1, face[2] + 1));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = "v 0 0 0\nv 1 0 0\nv 0 1 0\nv 1 1 0\nf 1 2 3\nf 2 4 3\n# comment\n";
#[test]
fn parses_triangles() {
let g = parse_obj(SAMPLE).expect("parse");
assert_eq!(g.vertex_count(), 4);
assert_eq!(g.triangle_count(), 2);
assert_eq!(g.indices, [0, 1, 2, 1, 3, 2]);
}
#[test]
fn parses_slash_forms_and_negative_indices() {
let text = "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1/1/1 2//1 -1\n";
let g = parse_obj(text).expect("parse");
assert_eq!(g.triangle_count(), 1);
assert_eq!(g.indices, [0, 1, 2]);
}
#[test]
fn rejects_polygon_face() {
let text = "v 0 0 0\nv 1 0 0\nv 0 1 0\nv 1 1 0\nf 1 2 3 4\n";
let err = parse_obj(text).unwrap_err();
assert!(err.to_string().contains("only triangle faces"), "{err}");
}
#[test]
fn rejects_out_of_range_index() {
let text = "v 0 0 0\nf 1 2 3\n";
let err = parse_obj(text).unwrap_err();
assert!(err.to_string().contains("beyond"), "{err}");
}
#[test]
fn rejects_garbage_numbers() {
let text = "v zero 0 0\n";
assert!(parse_obj(text).is_err());
let text2 = "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 x 3\n";
assert!(parse_obj(text2).is_err());
}
#[test]
fn rejects_non_finite_vertex() {
let text = "v nan 0 0\n";
assert!(parse_obj(text).is_err());
}
#[test]
fn empty_input_is_empty_mesh() {
let g = parse_obj("").expect("parse");
assert_eq!(g.triangle_count(), 0);
}
#[test]
fn obj_roundtrip_text() {
let g = parse_obj(SAMPLE).expect("parse");
let text = write_obj(&g, "roundtrip");
let g2 = parse_obj(&text).expect("reparse");
assert_eq!(g.positions, g2.positions);
assert_eq!(g.indices, g2.indices);
}
}