nmarshall's picture
obj: computed export
3f3b143
Raw History Blame Contribute Delete
7.41 kB
//! Bounded OBJ export of explicit computed geometry; editable data is not stored.
use mb3_dat::{Limits, Vec3, World};
use std::{collections::HashMap, fmt};
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
ComputedRequired,
InvalidGeometry,
NonFinite,
Overflow,
Limit,
Allocation,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "OBJ: {self:?}")
}
}
impl std::error::Error for Error {}
type Result<T> = std::result::Result<T, Error>;
struct Output<'a> {
bytes: Vec<u8>,
limits: &'a Limits,
items: usize,
owned: usize,
error: Option<Error>,
}
impl fmt::Write for Output<'_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let result = (|| {
let n = self
.bytes
.len()
.checked_add(s.len())
.ok_or(Error::Overflow)?;
if n > self.limits.max_bytes {
return Err(Error::Limit);
}
self.bytes
.try_reserve(s.len())
.map_err(|_| Error::Allocation)?;
self.bytes.extend_from_slice(s.as_bytes());
Ok(())
})();
if let Err(e) = result {
self.error = Some(e);
return Err(fmt::Error);
}
Ok(())
}
}
impl Output<'_> {
fn put(&mut self, args: fmt::Arguments<'_>) -> Result<()> {
fmt::write(self, args).map_err(|_| self.error.take().unwrap_or(Error::Overflow))
}
fn charge(&mut self, n: usize) -> Result<()> {
self.items = self.items.checked_add(n).ok_or(Error::Overflow)?;
if self.items > self.limits.max_items {
Err(Error::Limit)
} else {
Ok(())
}
}
fn vertex(&mut self, v: Vec3) -> Result<()> {
key(v)?;
self.put(format_args!("v {} {} {}\n", v[0], v[1], v[2]))
}
}
fn key(v: Vec3) -> Result<[u32; 3]> {
if !v.iter().all(|n| n.is_finite()) {
return Err(Error::NonFinite);
}
Ok(v.map(|n| if n == 0. { 0 } else { n.to_bits() }))
}
fn mul(a: usize, b: usize) -> Result<usize> {
a.checked_mul(b).ok_or(Error::Overflow)
}
fn add(a: usize, b: usize) -> Result<usize> {
a.checked_add(b).ok_or(Error::Overflow)
}
pub fn encode(world: &World, limits: &Limits) -> Result<Vec<u8>> {
if !world.computed {
return Err(Error::ComputedRequired);
}
let mut o = Output {
bytes: Vec::new(),
limits,
items: 0,
owned: 0,
error: None,
};
let mut base = 1usize;
o.charge(add(world.brushes.len(), world.patches.len())?)?;
for brush in &world.brushes {
o.charge(brush.planes.len())?;
o.put(format_args!("#brush\n"))?;
let mut map = HashMap::new();
for p in &brush.planes {
let vertices = p.polygon.as_ref().ok_or(Error::InvalidGeometry)?;
if vertices.len() < 3 {
return Err(Error::InvalidGeometry);
}
o.charge(vertices.len())?;
for v in vertices {
let k = key(*v)?;
if !map.contains_key(&k) {
o.owned = add(o.owned, 2 * std::mem::size_of::<([u32; 3], usize)>())?;
if o.owned > limits.max_owned_bytes {
return Err(Error::Limit);
}
map.try_reserve(1).map_err(|_| Error::Allocation)?;
let id = add(base, map.len())?;
map.insert(k, id);
o.vertex(*v)?;
}
}
}
for p in &brush.planes {
o.put(format_args!("f"))?;
for v in p.polygon.as_ref().ok_or(Error::InvalidGeometry)? {
let id = map.get(&key(*v)?).ok_or(Error::InvalidGeometry)?;
o.put(format_args!(" {id}"))?;
}
o.put(format_args!("\n"))?;
}
base = add(base, map.len())?;
}
for p in &world.patches {
let data = p.computed.as_ref().ok_or(Error::InvalidGeometry)?;
let u = add(mul(p.anchors_u as usize / 2, p.quads_u as usize)?, 1)?;
let v = add(mul(p.anchors_v as usize / 2, p.quads_v as usize)?, 1)?;
let n = mul(u, v)?;
if data.points.len() != n
|| data.normals.len() != n
|| p.anchors.len() != mul(p.anchors_u as usize, p.anchors_v as usize)?
{
return Err(Error::InvalidGeometry);
}
o.charge(add(
add(p.anchors.len(), data.normals.len())?,
add(n, mul(u - 1, v - 1)?)?,
)?)?;
for v in p.anchors.iter().chain(&data.normals) {
key(*v)?;
}
o.put(format_args!("#patch9\n"))?;
for point in &data.points {
o.vertex(*point)?;
}
for y in 0..v - 1 {
for x in 0..u - 1 {
let i = add(base, add(x, mul(y, u)?)?)?;
o.put(format_args!(
"f {} {} {} {}\n",
i,
add(i, u)?,
add(add(i, u)?, 1)?,
add(i, 1)?
))?;
}
}
base = add(base, n)?;
}
Ok(o.bytes)
}
#[cfg(test)]
mod tests {
use super::*;
use mb3_dat::{Brush, Plane};
fn world() -> World {
World {
computed: true,
brushes: vec![Brush {
planes: vec![Plane {
polygon: Some(vec![[0., 0., 0.], [1., 0., 0.], [0., 1., 0.]]),
..Plane::default()
}],
}],
..World::default()
}
}
#[test]
fn exact_triangle_and_limits() {
let w = world();
assert_eq!(
encode(&w, &Limits::default()).unwrap(),
b"#brush\nv 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n"
);
for l in [
Limits {
max_bytes: 2,
..Limits::default()
},
Limits {
max_items: 1,
..Limits::default()
},
Limits {
max_owned_bytes: 1,
..Limits::default()
},
] {
assert!(encode(&w, &l).is_err());
}
}
#[test]
fn no_global_dedup_and_signed_zero_equality() {
let mut w = world();
w.brushes[0].planes.push(Plane {
polygon: Some(vec![[-0., 0., 0.], [1., 0., 0.], [0., 1., 0.]]),
..Plane::default()
});
w.brushes.push(w.brushes[0].clone());
let s = String::from_utf8(encode(&w, &Limits::default()).unwrap()).unwrap();
assert_eq!(s.lines().filter(|s| s.starts_with("v ")).count(), 6);
assert!(s.contains("f 4 5 6"));
}
#[test]
fn invalid_models_refused() {
let mut w = world();
w.computed = false;
assert_eq!(
encode(&w, &Limits::default()).unwrap_err(),
Error::ComputedRequired
);
w = world();
w.brushes[0].planes[0].polygon.as_mut().unwrap()[0][0] = f32::NAN;
assert_eq!(
encode(&w, &Limits::default()).unwrap_err(),
Error::NonFinite
);
w = world();
w.brushes[0].planes[0].polygon = Some(vec![]);
assert!(encode(&w, &Limits::default()).is_err());
}
}