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            printable: None,
233        });
234
235        Ok(model)
236    }
237}
238
239/// Exports 3MF [`Model`] structures to Wavefront OBJ files.
240///
241/// The exporter writes all mesh objects from build items to OBJ format, creating
242/// separate groups for each object and applying build item transformations.
243///
244/// [`Model`]: lib3mf_core::model::Model
245pub struct ObjExporter;
246
247impl ObjExporter {
248    /// Writes a 3MF [`Model`] to OBJ text format.
249    ///
250    /// # Arguments
251    ///
252    /// * `model` - The 3MF model to export
253    /// * `writer` - Any type implementing [`Write`] to receive OBJ text data
254    ///
255    /// # Returns
256    ///
257    /// `Ok(())` on successful export.
258    ///
259    /// # Errors
260    ///
261    /// Returns [`Lib3mfError::Io`] if any write operation fails.
262    ///
263    /// # Format Details
264    ///
265    /// - **Groups**: Each mesh object creates an OBJ group (`g`) with the object's name or "Object"
266    /// - **Vertex indices**: Written as 1-based indices (OBJ convention)
267    /// - **Transformations**: Build item transforms are applied to vertex coordinates
268    /// - **Materials**: Not exported (OBJ output is geometry-only)
269    /// - **Normals/UVs**: Not exported
270    ///
271    /// # Behavior
272    ///
273    /// - Only mesh objects from `model.build.items` are exported
274    /// - Non-mesh geometries (Components, BooleanShape, etc.) are skipped
275    /// - Vertex indices are offset correctly across multiple objects
276    /// - Each object's vertices and faces are written in sequence
277    ///
278    /// # Examples
279    ///
280    /// ```no_run
281    /// use lib3mf_converters::obj::ObjExporter;
282    /// use lib3mf_core::model::Model;
283    /// use std::fs::File;
284    ///
285    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
286    /// # let model = Model::default();
287    /// let output = File::create("exported.obj")?;
288    /// ObjExporter::write(&model, output)?;
289    /// println!("Model exported successfully");
290    /// # Ok(())
291    /// # }
292    /// ```
293    ///
294    /// [`Model`]: lib3mf_core::model::Model
295    /// [`Lib3mfError::Io`]: lib3mf_core::error::Lib3mfError::Io
296    pub fn write<W: Write>(model: &Model, mut writer: W) -> Result<()> {
297        let mut vertex_offset = 1;
298
299        for item in &model.build.items {
300            if let Some(object) = model.resources.get_object(item.object_id)
301                && let lib3mf_core::model::Geometry::Mesh(mesh) = &object.geometry
302            {
303                let transform = item.transform;
304
305                writeln!(writer, "g {}", object.name.as_deref().unwrap_or("Object"))
306                    .map_err(Lib3mfError::Io)?;
307
308                // Write vertices
309                for v in &mesh.vertices {
310                    let p = transform.transform_point3(glam::Vec3::new(v.x, v.y, v.z));
311                    writeln!(writer, "v {} {} {}", p.x, p.y, p.z).map_err(Lib3mfError::Io)?;
312                }
313
314                // Write faces
315                for tri in &mesh.triangles {
316                    writeln!(
317                        writer,
318                        "f {} {} {}",
319                        tri.v1 + vertex_offset,
320                        tri.v2 + vertex_offset,
321                        tri.v3 + vertex_offset
322                    )
323                    .map_err(Lib3mfError::Io)?;
324                }
325
326                vertex_offset += mesh.vertices.len() as u32;
327            }
328        }
329        Ok(())
330    }
331}