use std::io::BufRead;
use crate::data::{AnyDataArray, CellArray, DataArray, Points, PolyData};
pub struct OffReader<R: BufRead> {
reader: R,
}
impl<R: BufRead> OffReader<R> {
pub fn new(reader: R) -> Self {
Self { reader }
}
pub fn read(&mut self) -> Result<PolyData, String> {
let mut lines = Vec::new();
let mut line_buf = String::new();
loop {
line_buf.clear();
let n = self.reader.read_line(&mut line_buf).map_err(|e| e.to_string())?;
if n == 0 { break; }
let trimmed = line_buf.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
lines.push(trimmed.to_string());
}
if lines.is_empty() {
return Err("empty OFF file".into());
}
let mut idx = 0;
let header = &lines[idx];
let has_colors = header.starts_with("COFF") || header.starts_with("coff");
let has_normals = header.starts_with("NOFF") || header.starts_with("noff");
let is_off = header == "OFF" || header == "off" || has_colors || has_normals;
if !is_off {
if !header.contains("OFF") && !header.contains("off") {
return Err(format!("not an OFF file, got: {header}"));
}
}
idx += 1;
if idx >= lines.len() {
return Err("unexpected end of OFF file".into());
}
let counts: Vec<usize> = lines[idx].split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
if counts.len() < 2 {
return Err(format!("expected vertex/face counts, got: {}", lines[idx]));
}
let n_verts = counts[0];
let n_faces = counts[1];
idx += 1;
let mut points = Points::<f64>::new();
let mut colors: Vec<f64> = Vec::new();
for i in 0..n_verts {
if idx + i >= lines.len() {
return Err(format!("expected {n_verts} vertices, got {i}"));
}
let parts: Vec<f64> = lines[idx + i].split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
if parts.len() < 3 {
return Err(format!("vertex {i} has fewer than 3 coordinates"));
}
points.push([parts[0], parts[1], parts[2]]);
if has_colors && parts.len() >= 6 {
let r = if parts[3] > 1.0 { parts[3] / 255.0 } else { parts[3] };
let g = if parts[4] > 1.0 { parts[4] / 255.0 } else { parts[4] };
let b = if parts[5] > 1.0 { parts[5] / 255.0 } else { parts[5] };
colors.push(r);
colors.push(g);
colors.push(b);
}
}
idx += n_verts;
let mut polys = CellArray::new();
for i in 0..n_faces {
if idx + i >= lines.len() {
return Err(format!("expected {n_faces} faces, got {i}"));
}
let parts: Vec<i64> = lines[idx + i].split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
if parts.is_empty() {
continue;
}
let n = parts[0] as usize;
if parts.len() < n + 1 {
return Err(format!("face {i}: expected {n} indices, got {}", parts.len() - 1));
}
let ids: Vec<i64> = parts[1..=n].to_vec();
polys.push_cell(&ids);
}
let mut mesh = PolyData::new();
mesh.points = points;
mesh.polys = polys;
if !colors.is_empty() && colors.len() == n_verts * 3 {
let arr = DataArray::from_vec("Colors", colors, 3);
mesh.point_data_mut().add_array(AnyDataArray::F64(arr));
mesh.point_data_mut().set_active_scalars("Colors");
}
Ok(mesh)
}
}
pub fn read_off_file(path: &std::path::Path) -> Result<PolyData, String> {
let file = std::fs::File::open(path).map_err(|e| e.to_string())?;
let reader = std::io::BufReader::new(file);
OffReader::new(reader).read()
}
pub fn write_off_file(mesh: &PolyData, path: &std::path::Path) -> Result<(), String> {
let file = std::fs::File::create(path).map_err(|e| e.to_string())?;
let mut writer = std::io::BufWriter::new(file);
crate::io::off::OffWriter::new(&mut writer).write(mesh).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_simple_off() {
let data = b"OFF\n3 1 0\n0 0 0\n1 0 0\n0 1 0\n3 0 1 2\n";
let mut reader = OffReader::new(&data[..]);
let mesh = reader.read().unwrap();
assert_eq!(mesh.points.len(), 3);
assert_eq!(mesh.polys.num_cells(), 1);
}
#[test]
fn read_coff() {
let data = b"COFF\n3 1 0\n0 0 0 255 0 0 255\n1 0 0 0 255 0 255\n0 1 0 0 0 255 255\n3 0 1 2\n";
let mut reader = OffReader::new(&data[..]);
let mesh = reader.read().unwrap();
assert_eq!(mesh.points.len(), 3);
assert!(mesh.point_data().scalars().is_some());
}
#[test]
fn roundtrip() {
let mesh = PolyData::from_triangles(
vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]],
vec![[0, 1, 2]],
);
let mut buf = Vec::new();
crate::io::off::OffWriter::new(&mut buf).write(&mesh).unwrap();
let mut reader = OffReader::new(&buf[..]);
let loaded = reader.read().unwrap();
assert_eq!(loaded.points.len(), 3);
assert_eq!(loaded.polys.num_cells(), 1);
let p = loaded.points.get(0);
assert!((p[0] - 1.0).abs() < 1e-6);
assert!((p[1] - 2.0).abs() < 1e-6);
assert!((p[2] - 3.0).abs() < 1e-6);
}
#[test]
fn quad_roundtrip() {
let mesh = PolyData::from_quads(
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]],
vec![[0, 1, 2, 3]],
);
let mut buf = Vec::new();
crate::io::off::OffWriter::new(&mut buf).write(&mesh).unwrap();
let loaded = OffReader::new(&buf[..]).read().unwrap();
assert_eq!(loaded.points.len(), 4);
assert_eq!(loaded.polys.num_cells(), 1);
}
#[test]
fn comments_and_blank_lines() {
let data = b"# This is a comment\nOFF\n# another comment\n\n3 1 0\n0 0 0\n1 0 0\n0 1 0\n3 0 1 2\n";
let mesh = OffReader::new(&data[..]).read().unwrap();
assert_eq!(mesh.points.len(), 3);
}
}