//! 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 = std::result::Result; struct Output<'a> { bytes: Vec, limits: &'a Limits, items: usize, owned: usize, error: Option, } 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 { a.checked_mul(b).ok_or(Error::Overflow) } fn add(a: usize, b: usize) -> Result { a.checked_add(b).ok_or(Error::Overflow) } pub fn encode(world: &World, limits: &Limits) -> Result> { 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()); } }