Skip to main content

geometry_dag/
obj.rs

1//! Wavefront OBJ 解析(编译管线输入)。
2//!
3//! 支持子集:`v`(3-4 分量,w 忽略)、`f`(严格三角形;`v`、`v/vt`、`v//vn`、
4//! `v/vt/vn` 四种形式;1-based 正索引与负索引)。多边形面显式拒绝——离线编译
5//! 不允许静默三角化改变拓扑;需要时上游先用权威工具扇形化。
6
7use crate::error::{DagError, DagResult};
8use crate::types::IndexedGeometry;
9
10/// 解析 OBJ 文本为索引化三角形网格。
11///
12/// # Errors
13/// 非三角形面、非法分量数、索引越界、非法数值均返回带行号的 [`DagError::ObjParse`]。
14pub fn parse_obj(text: &str) -> DagResult<IndexedGeometry> {
15    let mut positions: Vec<f32> = Vec::new();
16    let mut raw_faces: Vec<[usize; 3]> = Vec::new();
17
18    for (line_no, raw_line) in text.lines().enumerate() {
19        let line_no = line_no + 1;
20        let line = raw_line.trim();
21        if line.is_empty() || line.starts_with('#') {
22            continue;
23        }
24        let mut tokens = line.split_whitespace();
25        let tag = tokens.next().unwrap_or_default();
26        match tag {
27            "v" => {
28                let coords: Vec<f32> = tokens
29                    .map(|t| parse_f32(t, line_no))
30                    .collect::<DagResult<_>>()?;
31                if coords.len() < 3 {
32                    return Err(DagError::ObjParse {
33                        line: line_no,
34                        reason: format!("vertex needs 3 (optionally 4 with w) components, got {}", coords.len()),
35                    });
36                }
37                if !coords[..3].iter().all(|v| v.is_finite()) {
38                    return Err(DagError::ObjParse { line: line_no, reason: "vertex component must be finite".into() });
39                }
40                positions.extend_from_slice(&coords[..3]);
41            }
42            "f" => {
43                let indices: Vec<usize> = tokens
44                    .map(|t| parse_face_vertex(t, line_no, positions.len() / 3))
45                    .collect::<DagResult<_>>()?;
46                if indices.len() != 3 {
47                    return Err(DagError::ObjParse {
48                        line: line_no,
49                        reason: format!("only triangle faces are supported, got {} vertices", indices.len()),
50                    });
51                }
52                raw_faces.push([indices[0], indices[1], indices[2]]);
53            }
54            // v/vt/vn、组合、渲染属性等标签与编译无关,跳过。
55            _ => {}
56        }
57    }
58
59    let mut indices = Vec::with_capacity(raw_faces.len() * 3);
60    for (face_no, face) in raw_faces.iter().enumerate() {
61        for &vertex in face {
62            if vertex >= positions.len() / 3 {
63                return Err(DagError::ObjParse {
64                    line: 0,
65                    reason: format!("face {} references vertex {} beyond {} defined", face_no + 1, vertex + 1, positions.len() / 3),
66                });
67            }
68            indices.push(vertex as u32);
69        }
70    }
71    Ok(IndexedGeometry { positions, indices })
72}
73
74/// 解析 f32 分量(fail-closed,含定位)。
75fn parse_f32(token: &str, line_no: usize) -> DagResult<f32> {
76    token
77        .parse::<f32>()
78        .map_err(|_| DagError::ObjParse { line: line_no, reason: format!("invalid number: {token:?}") })
79}
80
81/// 解析面顶点:`v`、`v/vt`、`v//vn`、`v/vt/vn`;1-based 正索引与负索引(相对当前)。
82/// 仅返回位置槽(vt/vn 槽位与编译无关)。
83fn parse_face_vertex(token: &str, line_no: usize, vertex_count: usize) -> DagResult<usize> {
84    let vertex_str = token.split('/').next().unwrap_or_default();
85    let raw: i64 = vertex_str
86        .parse::<i64>()
87        .map_err(|_| DagError::ObjParse { line: line_no, reason: format!("invalid face index: {token:?}") })?;
88    if raw == 0 {
89        return Err(DagError::ObjParse { line: line_no, reason: "face index 0 is not valid OBJ".into() });
90    }
91    if raw > 0 {
92        Ok((raw - 1) as usize)
93    } else {
94        if vertex_count == 0 {
95            return Err(DagError::ObjParse { line: line_no, reason: "negative face index before any vertex".into() });
96        }
97        let resolved = vertex_count as i64 + raw;
98        Ok(resolved as usize)
99    }
100}
101
102/// 将几何写为 OBJ 文本(诊断/回归对比用)。Rust f32 `Display` 本身保证 round-trip。
103#[must_use]
104pub fn write_obj(geometry: &IndexedGeometry, comment: &str) -> String {
105    let mut out = String::with_capacity(geometry.positions.len() * 16 + geometry.indices.len() * 16);
106    out.push_str("# ");
107    out.push_str(comment);
108    out.push('\n');
109    for tri in geometry.positions.chunks_exact(3) {
110        out.push_str(&format!("v {} {} {}\n", tri[0], tri[1], tri[2]));
111    }
112    for face in geometry.indices.chunks_exact(3) {
113        out.push_str(&format!("f {} {} {}\n", face[0] + 1, face[1] + 1, face[2] + 1));
114    }
115    out
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    const SAMPLE: &str = "v 0 0 0\nv 1 0 0\nv 0 1 0\nv 1 1 0\nf 1 2 3\nf 2 4 3\n# comment\n";
123
124    #[test]
125    fn parses_triangles() {
126        let g = parse_obj(SAMPLE).expect("parse");
127        assert_eq!(g.vertex_count(), 4);
128        assert_eq!(g.triangle_count(), 2);
129        assert_eq!(g.indices, [0, 1, 2, 1, 3, 2]);
130    }
131
132    #[test]
133    fn parses_slash_forms_and_negative_indices() {
134        let text = "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1/1/1 2//1 -1\n";
135        let g = parse_obj(text).expect("parse");
136        assert_eq!(g.triangle_count(), 1);
137        assert_eq!(g.indices, [0, 1, 2]);
138    }
139
140    #[test]
141    fn rejects_polygon_face() {
142        let text = "v 0 0 0\nv 1 0 0\nv 0 1 0\nv 1 1 0\nf 1 2 3 4\n";
143        let err = parse_obj(text).unwrap_err();
144        assert!(err.to_string().contains("only triangle faces"), "{err}");
145    }
146
147    #[test]
148    fn rejects_out_of_range_index() {
149        let text = "v 0 0 0\nf 1 2 3\n";
150        let err = parse_obj(text).unwrap_err();
151        assert!(err.to_string().contains("beyond"), "{err}");
152    }
153
154    #[test]
155    fn rejects_garbage_numbers() {
156        let text = "v zero 0 0\n";
157        assert!(parse_obj(text).is_err());
158        let text2 = "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 x 3\n";
159        assert!(parse_obj(text2).is_err());
160    }
161
162    #[test]
163    fn rejects_non_finite_vertex() {
164        let text = "v nan 0 0\n";
165        assert!(parse_obj(text).is_err());
166    }
167
168    #[test]
169    fn empty_input_is_empty_mesh() {
170        let g = parse_obj("").expect("parse");
171        assert_eq!(g.triangle_count(), 0);
172    }
173
174    #[test]
175    fn obj_roundtrip_text() {
176        let g = parse_obj(SAMPLE).expect("parse");
177        let text = write_obj(&g, "roundtrip");
178        let g2 = parse_obj(&text).expect("reparse");
179        assert_eq!(g.positions, g2.positions);
180        assert_eq!(g.indices, g2.indices);
181    }
182}