lib3mf_converters/
obj.rs

1//! Wavefront OBJ format import and export.
2//!
3//! This module provides conversion between OBJ files and 3MF [`Model`] structures.
4//!
5//! ## OBJ Format
6//!
7//! The Wavefront OBJ format is a text-based 3D geometry format. This implementation supports
8//! a basic subset of the full OBJ specification:
9//!
10//! **Supported features:**
11//! - `v` - Vertex positions (x, y, z)
12//! - `f` - Faces (vertex indices)
13//! - Polygon faces (automatically triangulated using fan triangulation)
14//!
15//! **Ignored features:**
16//! - `vt` - Texture coordinates
17//! - `vn` - Vertex normals
18//! - `g` - Group names
19//! - `usemtl` - Material references
20//! - `mtllib` - Material library files
21//!
22//! ## Examples
23//!
24//! ### Importing OBJ
25//!
26//! ```no_run
27//! use lib3mf_converters::obj::ObjImporter;
28//! use std::fs::File;
29//!
30//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
31//! let file = File::open("model.obj")?;
32//! let model = ObjImporter::read(file)?;
33//! println!("Imported model with {} build items", model.build.items.len());
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! ### Exporting OBJ
39//!
40//! ```no_run
41//! use lib3mf_converters::obj::ObjExporter;
42//! use lib3mf_core::model::Model;
43//! use std::fs::File;
44//!
45//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
46//! # let model = Model::default();
47//! let file = File::create("output.obj")?;
48//! ObjExporter::write(&model, file)?;
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! [`Model`]: lib3mf_core::model::Model
54
55use lib3mf_core::error::{Lib3mfError, Result};
56use lib3mf_core::model::resources::ResourceId;
57use lib3mf_core::model::{BuildItem, Mesh, Model, Triangle};
58use std::io::{BufRead, BufReader, Read, Write};
59
60/// Imports Wavefront OBJ files into 3MF [`Model`] structures.
61///
62/// Parses vertex positions (`v`) and faces (`f`) from OBJ text format. Polygonal faces
63/// with more than 3 vertices are automatically triangulated using fan triangulation.
64///
65/// [`Model`]: lib3mf_core::model::Model
66pub struct ObjImporter;
67
68impl ObjImporter {
69    /// Reads an OBJ file and converts it to a 3MF [`Model`].
70    ///
71    /// # Arguments
72    ///
73    /// * `reader` - Any type implementing [`Read`] containing OBJ text data
74    ///
75    /// # Returns
76    ///
77    /// A [`Model`] containing:
78    /// - Single mesh object with ResourceId(1) named "OBJ Import"
79    /// - All triangles from the OBJ file (polygons triangulated via fan method)
80    /// - All vertices from the OBJ file
81    /// - Single build item referencing the mesh object
82    ///
83    /// # Errors
84    ///
85    /// Returns [`Lib3mfError::Validation`] if:
86    /// - Vertex line has fewer than 4 fields (v x y z)
87    /// - Face line has fewer than 4 fields (f v1 v2 v3...)
88    /// - Float parsing fails for vertex coordinates
89    /// - Integer parsing fails for face indices
90    /// - Relative indices (negative values) are used (not supported)
91    ///
92    /// Returns [`Lib3mfError::Io`] if reading from the input fails.
93    ///
94    /// # Format Details
95    ///
96    /// - **Index conversion**: OBJ uses 1-based indices, converted to 0-based for internal mesh
97    /// - **Fan triangulation**: Polygons with N vertices create N-2 triangles using first vertex as fan apex
98    /// - **Ignored elements**: Texture coords (vt), normals (vn), groups (g), materials (usemtl, mtllib)
99    /// - **Comments**: Lines starting with `#` are skipped
100    ///
101    /// # Examples
102    ///
103    /// ```no_run
104    /// use lib3mf_converters::obj::ObjImporter;
105    /// use std::fs::File;
106    ///
107    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
108    /// let file = File::open("cube.obj")?;
109    /// let model = ObjImporter::read(file)?;
110    ///
111    /// // Access the imported mesh
112    /// let obj = model.resources.get_object(lib3mf_core::model::resources::ResourceId(1))
113    ///     .expect("OBJ import creates object with ID 1");
114    /// if let lib3mf_core::model::Geometry::Mesh(mesh) = &obj.geometry {
115    ///     println!("Imported {} vertices, {} triangles",
116    ///         mesh.vertices.len(), mesh.triangles.len());
117    /// }
118    /// # Ok(())
119    /// # }
120    /// ```
121    ///
122    /// [`Model`]: lib3mf_core::model::Model
123    /// [`Lib3mfError::Validation`]: lib3mf_core::error::Lib3mfError::Validation
124    /// [`Lib3mfError::Io`]: lib3mf_core::error::Lib3mfError::Io
125    pub fn read<R: Read>(reader: R) -> Result<Model> {
126        let mut reader = BufReader::new(reader);
127        let mut line = String::new();
128
129        let mut mesh = Mesh::default();
130
131        // OBJ indices are 1-based
132        // Mesh stores vertices in order added.
133
134        while reader.read_line(&mut line).map_err(Lib3mfError::Io)? > 0 {
135            let trimmed = line.trim();
136            if trimmed.is_empty() || trimmed.starts_with('#') {
137                line.clear();
138                continue;
139            }
140
141            let parts: Vec<&str> = trimmed.split_whitespace().collect();
142            if parts.is_empty() {
143                line.clear();
144                continue;
145            }
146
147            match parts[0] {
148                "v" => {
149                    if parts.len() < 4 {
150                        return Err(Lib3mfError::Validation("Invalid OBJ vertex".to_string()));
151                    }
152                    let x = parts[1]
153                        .parse::<f32>()
154                        .map_err(|_| Lib3mfError::Validation("Invalid float".to_string()))?;
155                    let y = parts[2]
156                        .parse::<f32>()
157                        .map_err(|_| Lib3mfError::Validation("Invalid float".to_string()))?;
158                    let z = parts[3]
159                        .parse::<f32>()
160                        .map_err(|_| Lib3mfError::Validation("Invalid float".to_string()))?;
161                    mesh.add_vertex(x, y, z);
162                }
163                "f" => {
164                    if parts.len() < 4 {
165                        // Skipping point/line elements
166                        line.clear();
167                        continue;
168                    }
169
170                    let mut indices = Vec::new();
171                    for part in &parts[1..] {
172                        // Format: v, v/vt, v/vt/vn, v//vn
173                        let subparts: Vec<&str> = part.split('/').collect();
174                        let v_idx_str = subparts[0];
175                        let v_idx = v_idx_str
176                            .parse::<i32>()
177                            .map_err(|_| Lib3mfError::Validation("Invalid index".to_string()))?;
178
179                        let idx = if v_idx > 0 {
180                            (v_idx - 1) as u32
181                        } else {
182                            // Relative index
183                            return Err(Lib3mfError::Validation(
184                                "Relative OBJ indices not supported yet".to_string(),
185                            ));
186                        };
187                        indices.push(idx);
188                    }
189
190                    // Triangulate fan
191                    if indices.len() >= 3 {
192                        for i in 1..indices.len() - 1 {
193                            mesh.triangles.push(Triangle {
194                                v1: indices[0],
195                                v2: indices[i],
196                                v3: indices[i + 1],
197                                ..Default::default()
198                            });
199                        }
200                    }
201                }
202                _ => {} // Ignore vt, vn, g, usemtl etc. for now
203            }
204
205            line.clear();
206        }
207
208        let mut model = Model::default();
209        let resource_id = ResourceId(1);
210
211        let object = lib3mf_core::model::Object {
212            id: resource_id,
213            object_type: lib3mf_core::model::ObjectType::Model,
214            name: Some("OBJ Import".to_string()),
215            part_number: None,
216            uuid: None,
217            pid: None,
218            pindex: None,
219            thumbnail: None,
220            geometry: lib3mf_core::model::Geometry::Mesh(mesh),
221        };
222
223        // Handle result
224        let _ = model.resources.add_object(object);
225
226        model.build.items.push(BuildItem {
227            object_id: resource_id,
228            transform: glam::Mat4::IDENTITY,
229            part_number: None,
230            uuid: None,
231            path: None,
232        });
233
234        Ok(model)
235    }
236}
237
238/// Exports 3MF [`Model`] structures to Wavefront OBJ files.
239///
240/// The exporter writes all mesh objects from build items to OBJ format, creating
241/// separate groups for each object and applying build item transformations.
242///
243/// [`Model`]: lib3mf_core::model::Model
244pub struct ObjExporter;
245
246impl ObjExporter {
247    /// Writes a 3MF [`Model`] to OBJ text format.
248    ///
249    /// # Arguments
250    ///
251    /// * `model` - The 3MF model to export
252    /// * `writer` - Any type implementing [`Write`] to receive OBJ text data
253    ///
254    /// # Returns
255    ///
256    /// `Ok(())` on successful export.
257    ///
258    /// # Errors
259    ///
260    /// Returns [`Lib3mfError::Io`] if any write operation fails.
261    ///
262    /// # Format Details
263    ///
264    /// - **Groups**: Each mesh object creates an OBJ group (`g`) with the object's name or "Object"
265    /// - **Vertex indices**: Written as 1-based indices (OBJ convention)
266    /// - **Transformations**: Build item transforms are applied to vertex coordinates
267    /// - **Materials**: Not exported (OBJ output is geometry-only)
268    /// - **Normals/UVs**: Not exported
269    ///
270    /// # Behavior
271    ///
272    /// - Only mesh objects from `model.build.items` are exported
273    /// - Non-mesh geometries (Components, BooleanShape, etc.) are skipped
274    /// - Vertex indices are offset correctly across multiple objects
275    /// - Each object's vertices and faces are written in sequence
276    ///
277    /// # Examples
278    ///
279    /// ```no_run
280    /// use lib3mf_converters::obj::ObjExporter;
281    /// use lib3mf_core::model::Model;
282    /// use std::fs::File;
283    ///
284    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
285    /// # let model = Model::default();
286    /// let output = File::create("exported.obj")?;
287    /// ObjExporter::write(&model, output)?;
288    /// println!("Model exported successfully");
289    /// # Ok(())
290    /// # }
291    /// ```
292    ///
293    /// [`Model`]: lib3mf_core::model::Model
294    /// [`Lib3mfError::Io`]: lib3mf_core::error::Lib3mfError::Io
295    pub fn write<W: Write>(model: &Model, mut writer: W) -> Result<()> {
296        let mut vertex_offset = 1;
297
298        for item in &model.build.items {
299            if let Some(object) = model.resources.get_object(item.object_id)
300                && let lib3mf_core::model::Geometry::Mesh(mesh) = &object.geometry
301            {
302                let transform = item.transform;
303
304                writeln!(writer, "g {}", object.name.as_deref().unwrap_or("Object"))
305                    .map_err(Lib3mfError::Io)?;
306
307                // Write vertices
308                for v in &mesh.vertices {
309                    let p = transform.transform_point3(glam::Vec3::new(v.x, v.y, v.z));
310                    writeln!(writer, "v {} {} {}", p.x, p.y, p.z).map_err(Lib3mfError::Io)?;
311                }
312
313                // Write faces
314                for tri in &mesh.triangles {
315                    writeln!(
316                        writer,
317                        "f {} {} {}",
318                        tri.v1 + vertex_offset,
319                        tri.v2 + vertex_offset,
320                        tri.v3 + vertex_offset
321                    )
322                    .map_err(Lib3mfError::Io)?;
323                }
324
325                vertex_offset += mesh.vertices.len() as u32;
326            }
327        }
328        Ok(())
329    }
330}