ferravex_graphics/
obj.rs

1use crate::{Color, Mesh, Vec3, Vertex, vec3};
2
3#[derive(Clone, Copy, Debug)]
4pub struct ObjLimits {
5    pub max_bytes: usize,
6    pub max_positions: usize,
7    pub max_faces: usize,
8    pub max_face_vertices: usize,
9    pub max_output_vertices: usize,
10}
11impl Default for ObjLimits {
12    fn default() -> Self {
13        Self {
14            max_bytes: 8 * 1024 * 1024,
15            max_positions: 100_000,
16            max_faces: 100_000,
17            max_face_vertices: 32,
18            max_output_vertices: 750_000,
19        }
20    }
21}
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum ObjErrorKind {
24    ResourceLimit,
25    InvalidRecord,
26    InvalidNumber,
27    InvalidIndex,
28    DegenerateFace,
29    NonPlanarFace,
30    ConcaveFace,
31    Empty,
32}
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub struct ObjError {
35    pub line: usize,
36    pub kind: ObjErrorKind,
37}
38impl std::fmt::Display for ObjError {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        write!(f, "OBJ line {}: {:?}", self.line, self.kind)
41    }
42}
43impl std::error::Error for ObjError {}
44impl Mesh {
45    /// Bounded OBJ subset: positions, UVs/normals for index validation, and convex planar faces.
46    /// Output deliberately uses flat generated normals and the supplied solid color.
47    pub fn from_obj(source: &str, color: Color) -> Result<Self, ObjError> {
48        Self::from_obj_with_limits(source, color, ObjLimits::default())
49    }
50    pub fn from_obj_with_limits(
51        source: &str,
52        color: Color,
53        limits: ObjLimits,
54    ) -> Result<Self, ObjError> {
55        if source.len() > limits.max_bytes || !color.to_vec().is_finite() {
56            return Err(ObjError {
57                line: 0,
58                kind: ObjErrorKind::ResourceLimit,
59            });
60        }
61        let mut positions = Vec::new();
62        let mut normals = 0;
63        let mut uvs = 0;
64        let mut faces = 0;
65        let mut output = Self {
66            vertices: Vec::new(),
67            indices: Vec::new(),
68        };
69        for (number, line) in source.lines().enumerate() {
70            let number = number + 1;
71            let line = line.split('#').next().unwrap_or("");
72            let mut words = line.split_whitespace();
73            let Some(record) = words.next() else {
74                continue;
75            };
76            let fail = |kind| ObjError { line: number, kind };
77            match record {
78                "v" => {
79                    if positions.len() >= limits.max_positions {
80                        return Err(fail(ObjErrorKind::ResourceLimit));
81                    }
82                    let values = parse_values(words, 3, 3).map_err(fail)?;
83                    positions.push(vec3(values[0], values[1], values[2]));
84                }
85                "vn" => {
86                    parse_values(words, 3, 3).map_err(fail)?;
87                    normals += 1;
88                    if normals > limits.max_positions {
89                        return Err(fail(ObjErrorKind::ResourceLimit));
90                    }
91                }
92                "vt" => {
93                    parse_values(words, 1, 3).map_err(fail)?;
94                    uvs += 1;
95                    if uvs > limits.max_positions {
96                        return Err(fail(ObjErrorKind::ResourceLimit));
97                    }
98                }
99                "f" => {
100                    faces += 1;
101                    if faces > limits.max_faces {
102                        return Err(fail(ObjErrorKind::ResourceLimit));
103                    }
104                    let mut points = Vec::new();
105                    for word in words {
106                        if points.len() >= limits.max_face_vertices.min(1024) {
107                            return Err(fail(ObjErrorKind::ResourceLimit));
108                        }
109                        let mut fields = word.split('/');
110                        let position = parse_index(fields.next().unwrap_or(""), positions.len())
111                            .map_err(fail)?;
112                        if let Some(uv) = fields.next() {
113                            if !uv.is_empty() {
114                                parse_index(uv, uvs).map_err(fail)?;
115                            }
116                            if let Some(normal) = fields.next() {
117                                if normal.is_empty() {
118                                    return Err(fail(ObjErrorKind::InvalidIndex));
119                                }
120                                parse_index(normal, normals).map_err(fail)?;
121                            } else if uv.is_empty() {
122                                return Err(fail(ObjErrorKind::InvalidIndex));
123                            }
124                        }
125                        if fields.next().is_some() {
126                            return Err(fail(ObjErrorKind::InvalidIndex));
127                        }
128                        points.push(positions[position]);
129                    }
130                    if points.len() < 3 {
131                        return Err(fail(ObjErrorKind::InvalidRecord));
132                    }
133                    let required = (points.len() - 2) * 3;
134                    if output.vertices.len().saturating_add(required)
135                        > limits.max_output_vertices.min(1_000_000)
136                    {
137                        return Err(fail(ObjErrorKind::ResourceLimit));
138                    }
139                    let normal = validate_polygon(&points).map_err(fail)?;
140                    for i in 1..points.len() - 1 {
141                        let a = points[0];
142                        let b = points[i];
143                        let c = points[i + 1];
144                        if (b - a).cross(c - a).length_squared() <= 1.0e-20 {
145                            return Err(fail(ObjErrorKind::DegenerateFace));
146                        }
147                        let start = output.vertices.len() as u32;
148                        output.vertices.extend([
149                            Vertex::new(a, normal, color),
150                            Vertex::new(b, normal, color),
151                            Vertex::new(c, normal, color),
152                        ]);
153                        output.indices.extend([start, start + 1, start + 2]);
154                    }
155                }
156                // Names/smoothing/material references are accepted as metadata; no paths are opened.
157                "o" | "g" | "s" | "usemtl" | "mtllib" => {}
158                _ => return Err(fail(ObjErrorKind::InvalidRecord)),
159            }
160        }
161        if output.vertices.is_empty() {
162            return Err(ObjError {
163                line: 0,
164                kind: ObjErrorKind::Empty,
165            });
166        }
167        Ok(output)
168    }
169}
170fn parse_values<'a>(
171    words: impl Iterator<Item = &'a str>,
172    min: usize,
173    max: usize,
174) -> Result<Vec<f32>, ObjErrorKind> {
175    let mut result = Vec::with_capacity(max);
176    for word in words {
177        if result.len() >= max {
178            return Err(ObjErrorKind::InvalidRecord);
179        }
180        let value = word
181            .parse::<f32>()
182            .map_err(|_| ObjErrorKind::InvalidNumber)?;
183        if !value.is_finite() {
184            return Err(ObjErrorKind::InvalidNumber);
185        }
186        result.push(value);
187    }
188    if result.len() < min {
189        return Err(ObjErrorKind::InvalidRecord);
190    }
191    Ok(result)
192}
193fn parse_index(word: &str, count: usize) -> Result<usize, ObjErrorKind> {
194    let raw = word
195        .parse::<i64>()
196        .map_err(|_| ObjErrorKind::InvalidIndex)?;
197    let index = if raw > 0 {
198        raw - 1
199    } else if raw < 0 {
200        (count as i64)
201            .checked_add(raw)
202            .ok_or(ObjErrorKind::InvalidIndex)?
203    } else {
204        return Err(ObjErrorKind::InvalidIndex);
205    };
206    if index < 0 || index as usize >= count {
207        Err(ObjErrorKind::InvalidIndex)
208    } else {
209        Ok(index as usize)
210    }
211}
212fn validate_polygon(points: &[Vec3]) -> Result<Vec3, ObjErrorKind> {
213    let origin = points[0];
214    let normal = (1..points.len() - 1)
215        .find_map(|i| {
216            (points[i] - origin)
217                .cross(points[i + 1] - origin)
218                .try_normalize()
219        })
220        .ok_or(ObjErrorKind::DegenerateFace)?;
221    let scale = points
222        .iter()
223        .map(|p| p.distance(origin))
224        .fold(1.0_f32, f32::max);
225    let epsilon = scale * 1.0e-5;
226    if !scale.is_finite() {
227        return Err(ObjErrorKind::InvalidNumber);
228    }
229    if points
230        .iter()
231        .any(|&p| (p - origin).dot(normal).abs() > epsilon)
232    {
233        return Err(ObjErrorKind::NonPlanarFace);
234    }
235    for i in 0..points.len() {
236        let a = points[i];
237        let b = points[(i + 1) % points.len()];
238        // Every vertex must lie on the interior side of every oriented edge.
239        // This also rejects self-intersecting faces with locally consistent turns.
240        if points
241            .iter()
242            .any(|&p| (b - a).cross(p - a).dot(normal) < -epsilon * epsilon)
243        {
244            return Err(ObjErrorKind::ConcaveFace);
245        }
246    }
247    Ok(normal)
248}