use std::path::Path;
use crate::error::ReadError;
use crate::mesh::{Grouping, Limits, Mesh};
const SKIPPED: &[&str] = &[
"vt",
"vn",
"s",
"mg",
"usemtl",
"mtllib",
"usemap",
"maplib",
"bevel",
"c_interp",
"d_interp",
"lod",
"shadow_obj",
"trace_obj",
"ctech",
"stech",
"p",
"l",
];
const REFUSED: &[&str] = &[
"vp", "cstype", "deg", "bmat", "step", "curv", "curv2", "surf", "parm", "trim", "hole", "scrv",
"sp", "end", "con", "bsp", "bzp", "cdc", "cdp", "res", "call", "csh",
];
fn statements(text: &str) -> impl Iterator<Item = (usize, String)> + '_ {
let mut lines = (1..).zip(text.lines());
std::iter::from_fn(move || {
let mut pending: Option<(usize, String)> = None;
for (n, raw) in lines.by_ref() {
let line = raw.split('#').next().unwrap_or("");
let (body, goes_on) = match line.trim_end().strip_suffix('\\') {
Some(b) => (b, true),
None => (line, false),
};
let (start, mut joined) = pending.take().unwrap_or((n, String::new()));
joined.push(' ');
joined.push_str(body);
if !goes_on {
return Some((start, joined));
}
pending = Some((start, joined));
}
pending
})
}
fn number(word: &str, line: usize) -> Result<f64, ReadError> {
word.parse::<f64>()
.ok()
.filter(|x| x.is_finite())
.ok_or_else(|| ReadError::BadNumberOnLine { line, found: word.to_owned() })
}
fn vertex<'a>(
mesh: &mut Mesh,
words: impl Iterator<Item = &'a str>,
line: usize,
limits: Limits,
) -> Result<(), ReadError> {
let numbers = words.map(|w| number(w, line)).collect::<Result<Vec<_>, _>>()?;
let bad = ReadError::BadVertex { line, numbers: numbers.len() };
if ![3, 4, 6].contains(&numbers.len()) {
return Err(bad);
}
let (Some(x), Some(y), Some(z)) = (numbers.first(), numbers.get(1), numbers.get(2)) else {
return Err(bad);
};
if u64::try_from(mesh.vertices.len()).unwrap_or(u64::MAX) >= limits.vertices {
return Err(ReadError::TooLarge { what: "vertices", limit: limits.vertices });
}
mesh.vertices.push([*x, *y, *z]);
Ok(())
}
fn face<'a>(
mesh: &mut Mesh,
words: impl Iterator<Item = &'a str>,
line: usize,
limits: Limits,
furthest: &mut Option<(usize, i64)>,
) -> Result<(), ReadError> {
let read_so_far = i64::try_from(mesh.vertices.len()).unwrap_or(i64::MAX);
let mut corners = Vec::new();
for w in words {
let first = w.split('/').next().unwrap_or("");
let i: i64 = first.parse().map_err(|_| ReadError::BadFace { line })?;
let at = match i {
0 => return Err(ReadError::NoVertex { line, index: 0 }),
i if i < 0 => read_so_far + i,
i => {
if furthest.is_none_or(|(_, f)| i > f) {
*furthest = Some((line, i));
}
i - 1
}
};
corners.push(u32::try_from(at).map_err(|_| ReadError::NoVertex { line, index: i })?);
}
let Some((first, rest)) = corners.split_first() else {
return Err(ReadError::BadFace { line });
};
if rest.len() < 2 {
return Err(ReadError::BadFace { line });
}
for pair in rest.windows(2) {
if let [b, c] = pair {
if u64::try_from(mesh.triangles.len()).unwrap_or(u64::MAX) >= limits.triangles {
return Err(ReadError::TooLarge { what: "triangles", limit: limits.triangles });
}
mesh.triangles.push([*first, *b, *c]);
}
}
Ok(())
}
pub fn read(path: &Path, limits: Limits) -> Result<Mesh, ReadError> {
from_bytes(&limits.read_file(path)?, limits)
}
pub fn from_bytes(bytes: &[u8], limits: Limits) -> Result<Mesh, ReadError> {
limits.check_bytes(u64::try_from(bytes.len()).unwrap_or(u64::MAX))?;
let text = std::str::from_utf8(bytes).map_err(|_| ReadError::NotText)?;
let mut mesh = Mesh::default();
let mut furthest: Option<(usize, i64)> = None;
let (mut g, mut o): (Option<String>, Option<String>) = (None, None);
let mut grouping = Grouping::default();
let mut current: Option<u32> = None;
for (line, statement) in statements(text) {
let mut words = statement.split_ascii_whitespace();
match words.next() {
None => {}
Some("v") => vertex(&mut mesh, words, line, limits)?,
Some("f") => {
let before = mesh.triangles.len();
face(&mut mesh, words, line, limits, &mut furthest)?;
let id = match current {
Some(id) => id,
None => grouping.id(g.as_deref().or(o.as_deref()).unwrap_or("")),
};
current = Some(id);
grouping.take_id(id, mesh.triangles.len() - before);
}
Some("g") => {
let names = words.collect::<Vec<_>>().join(" ");
(g, current) = ((!names.is_empty()).then_some(names), None);
}
Some("o") => (o, current) = (Some(words.collect::<Vec<_>>().join(" ")), None),
Some(k) if SKIPPED.contains(&k) => {}
Some(k) if REFUSED.contains(&k) => {
return Err(ReadError::Unsupported { line, word: k.to_owned() });
}
Some(k) => return Err(ReadError::UnknownStatement { line, word: k.to_owned() }),
}
}
if let Some((line, i)) = furthest
&& i > i64::try_from(mesh.vertices.len()).unwrap_or(i64::MAX)
{
return Err(ReadError::NoVertex { line, index: i });
}
grouping.into(&mut mesh);
Ok(mesh)
}