use crate::error::{Error, Result};
use crate::format::Format;
use crate::io::parse_f64;
use crate::types::{Color, Face, Geometry, Mesh, Point, PointCloud, Vec3, Vertex};
use std::io::{BufRead, Write};
pub fn read<R: BufRead>(reader: &mut R) -> Result<Geometry> {
let mut vertices: Vec<Vertex> = Vec::new();
let mut normals: Vec<Vec3> = Vec::new();
let mut faces: Vec<Face> = Vec::new();
let mut comments = Vec::new();
let mut line = String::new();
let mut line_no = 0;
loop {
line.clear();
let bytes_read = reader.read_line(&mut line)?;
if bytes_read == 0 {
break;
}
line_no += 1;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with('#') {
comments.push(trimmed.trim_start_matches('#').trim().to_string());
continue;
}
let mut parts_buf = [""; 16];
let mut count = 0;
for part in trimmed.split_whitespace() {
if count < 16 {
parts_buf[count] = part;
count += 1;
} else {
break;
}
}
let parts = &parts_buf[..count];
match parts.first().copied() {
Some("v") => vertices.push(parse_vertex(line_no, &parts[1..])?),
Some("vn") => normals.push(parse_normal(line_no, &parts[1..])?),
Some("f") => parse_face(line_no, &parts[1..], vertices.len(), &mut faces)?,
Some("vt" | "g" | "o" | "s" | "usemtl" | "mtllib") => {}
Some(_) | None => {}
}
}
if faces.is_empty() {
let points = vertices
.into_iter()
.map(|vertex| {
let mut point = Point::new(vertex.position.x, vertex.position.y, vertex.position.z);
point.normal = vertex.normal;
point.color = vertex.color;
point
})
.collect();
let mut cloud = PointCloud::new(points);
cloud.metadata.source_format = Some(Format::Obj);
cloud.metadata.comments = comments;
cloud.metadata.point_count_hint = Some(cloud.points.len());
Ok(Geometry::PointCloud(cloud))
} else {
let mut mesh = Mesh::new(vertices, faces);
mesh.metadata.source_format = Some(Format::Obj);
mesh.metadata.comments = comments;
mesh.metadata.point_count_hint = Some(mesh.vertices.len());
if !normals.is_empty() {
mesh.metadata.warnings.push(
"OBJ normal indices were present; per-face/per-corner normals are not represented in the normalized mesh model".to_string(),
);
}
Ok(Geometry::Mesh(mesh))
}
}
pub fn write<W: Write>(writer: &mut W, geometry: &Geometry) -> Result<()> {
writeln!(writer, "# OBJ generated by point-formats")?;
match geometry {
Geometry::PointCloud(cloud) => {
writeln!(writer, "# point cloud exported as vertex-only OBJ; point attributes beyond optional color are non-standard in OBJ")?;
for point in &cloud.points {
write_vertex(writer, point.position, point.color)?;
}
}
Geometry::Mesh(mesh) => {
for vertex in &mesh.vertices {
write_vertex(writer, vertex.position, vertex.color)?;
}
for vertex in &mesh.vertices {
if let Some(normal) = vertex.normal {
write!(writer, "vn ")?;
crate::io::write_fmt_f64(writer, normal.x, 9)?;
write!(writer, " ")?;
crate::io::write_fmt_f64(writer, normal.y, 9)?;
write!(writer, " ")?;
crate::io::write_fmt_f64(writer, normal.z, 9)?;
writeln!(writer)?;
}
}
for face in &mesh.faces {
writeln!(
writer,
"f {} {} {}",
face.indices[0] + 1,
face.indices[1] + 1,
face.indices[2] + 1
)?;
}
}
}
Ok(())
}
fn parse_vertex(line_no: usize, parts: &[&str]) -> Result<Vertex> {
if parts.len() < 3 {
return Err(Error::parse(Format::Obj, line_no, "vertex requires x y z"));
}
let position = Vec3::new(
parse_f64(Format::Obj, line_no, "x", parts[0])?,
parse_f64(Format::Obj, line_no, "y", parts[1])?,
parse_f64(Format::Obj, line_no, "z", parts[2])?,
);
let mut vertex = Vertex::new(position);
if parts.len() >= 6 {
let r = parse_f64(Format::Obj, line_no, "red", parts[3])?;
let g = parse_f64(Format::Obj, line_no, "green", parts[4])?;
let b = parse_f64(Format::Obj, line_no, "blue", parts[5])?;
vertex.color =
if (0.0..=1.0).contains(&r) && (0.0..=1.0).contains(&g) && (0.0..=1.0).contains(&b) {
Color::from_unit_rgb(r, g, b)
} else {
Some(Color::new(
clamp_to_u16(r),
clamp_to_u16(g),
clamp_to_u16(b),
))
};
}
Ok(vertex)
}
fn parse_normal(line_no: usize, parts: &[&str]) -> Result<Vec3> {
if parts.len() < 3 {
return Err(Error::parse(Format::Obj, line_no, "normal requires x y z"));
}
Ok(Vec3::new(
parse_f64(Format::Obj, line_no, "nx", parts[0])?,
parse_f64(Format::Obj, line_no, "ny", parts[1])?,
parse_f64(Format::Obj, line_no, "nz", parts[2])?,
))
}
fn parse_face(
line_no: usize,
parts: &[&str],
vertex_count: usize,
faces: &mut Vec<Face>,
) -> Result<()> {
if parts.len() < 3 {
return Err(Error::parse(
Format::Obj,
line_no,
"face requires at least three vertices",
));
}
let indices: Vec<usize> = parts
.iter()
.map(|token| parse_face_index(line_no, token, vertex_count))
.collect::<Result<_>>()?;
for i in 1..(indices.len() - 1) {
faces.push(Face::new(indices[0], indices[i], indices[i + 1]));
}
Ok(())
}
fn parse_face_index(line_no: usize, token: &str, vertex_count: usize) -> Result<usize> {
let first = token.split('/').next().unwrap_or(token);
let idx = first.parse::<isize>().map_err(|_| {
Error::parse(
Format::Obj,
line_no,
format!("invalid face index '{token}'"),
)
})?;
if idx == 0 {
return Err(Error::parse(
Format::Obj,
line_no,
"OBJ indices are 1-based; 0 is invalid",
));
}
let zero_based = if idx > 0 {
(idx - 1) as usize
} else {
vertex_count.checked_sub((-idx) as usize).ok_or_else(|| {
Error::parse(
Format::Obj,
line_no,
format!("negative index '{idx}' is out of range"),
)
})?
};
if zero_based >= vertex_count {
return Err(Error::parse(
Format::Obj,
line_no,
format!("face index {idx} refers to missing vertex"),
));
}
Ok(zero_based)
}
fn write_vertex<W: Write>(writer: &mut W, position: Vec3, color: Option<Color>) -> Result<()> {
write!(writer, "v ")?;
crate::io::write_fmt_f64(writer, position.x, 9)?;
write!(writer, " ")?;
crate::io::write_fmt_f64(writer, position.y, 9)?;
write!(writer, " ")?;
crate::io::write_fmt_f64(writer, position.z, 9)?;
if let Some(color) = color {
let rgb = color.to_unit_rgb();
write!(writer, " ")?;
crate::io::write_fmt_f64(writer, rgb[0], 9)?;
write!(writer, " ")?;
crate::io::write_fmt_f64(writer, rgb[1], 9)?;
write!(writer, " ")?;
crate::io::write_fmt_f64(writer, rgb[2], 9)?;
}
writeln!(writer)?;
Ok(())
}
fn clamp_to_u16(value: f64) -> u16 {
if value <= 0.0 || !value.is_finite() {
0
} else if value >= u16::MAX as f64 {
u16::MAX
} else {
value.round() as u16
}
}