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}