lib3mf_converters/
stl.rs

1//! Binary STL format import and export.
2//!
3//! This module provides conversion between binary STL files and 3MF [`Model`] structures.
4//!
5//! ## STL Format
6//!
7//! The binary STL format consists of:
8//! - 80-byte header (typically unused, set to zeros)
9//! - 4-byte little-endian unsigned integer triangle count
10//! - For each triangle:
11//!   - 12 bytes: normal vector (3 × f32, often ignored by importers)
12//!   - 12 bytes: vertex 1 (x, y, z as f32)
13//!   - 12 bytes: vertex 2 (x, y, z as f32)
14//!   - 12 bytes: vertex 3 (x, y, z as f32)
15//!   - 2 bytes: attribute byte count (typically 0)
16//!
17//! **Note:** ASCII STL format is not supported.
18//!
19//! ## Examples
20//!
21//! ### Importing STL
22//!
23//! ```no_run
24//! use lib3mf_converters::stl::StlImporter;
25//! use std::fs::File;
26//!
27//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
28//! let file = File::open("model.stl")?;
29//! let model = StlImporter::read(file)?;
30//! println!("Imported {} vertices",
31//!     model.resources.iter_objects()
32//!         .filter_map(|obj| match &obj.geometry {
33//!             lib3mf_core::model::Geometry::Mesh(m) => Some(m.vertices.len()),
34//!             _ => None,
35//!         })
36//!         .sum::<usize>()
37//! );
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! ### Exporting STL
43//!
44//! ```no_run
45//! use lib3mf_converters::stl::StlExporter;
46//! use lib3mf_core::model::Model;
47//! use std::fs::File;
48//!
49//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
50//! # let model = Model::default();
51//! let file = File::create("output.stl")?;
52//! StlExporter::write(&model, file)?;
53//! # Ok(())
54//! # }
55//! ```
56//!
57//! [`Model`]: lib3mf_core::model::Model
58
59use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
60use lib3mf_core::error::{Lib3mfError, Result};
61use lib3mf_core::model::resources::ResourceId;
62use lib3mf_core::model::{BuildItem, Mesh, Model, Triangle, Vertex};
63use std::io::{Read, Write};
64
65/// Imports binary STL files into 3MF [`Model`] structures.
66///
67/// The importer reads binary STL format and creates a single mesh object with ResourceId(1).
68/// Vertices are deduplicated using bitwise float comparison during import.
69///
70/// [`Model`]: lib3mf_core::model::Model
71pub struct StlImporter;
72
73impl Default for StlImporter {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79impl StlImporter {
80    /// Creates a new STL importer instance.
81    pub fn new() -> Self {
82        Self
83    }
84
85    /// Reads a binary STL file and converts it to a 3MF [`Model`].
86    ///
87    /// # Arguments
88    ///
89    /// * `reader` - Any type implementing [`Read`] containing binary STL data
90    ///
91    /// # Returns
92    ///
93    /// A [`Model`] containing:
94    /// - Single mesh object with ResourceId(1) named "STL Import"
95    /// - All triangles from the STL file
96    /// - Deduplicated vertices (using bitwise float comparison)
97    /// - Single build item referencing the mesh object
98    ///
99    /// # Errors
100    ///
101    /// Returns [`Lib3mfError::Io`] if:
102    /// - Cannot read 80-byte header
103    /// - Cannot read triangle count
104    /// - Cannot read triangle data (normals, vertices, attribute bytes)
105    ///
106    /// Returns [`Lib3mfError::Validation`] if triangle count field cannot be parsed.
107    ///
108    /// # Format Details
109    ///
110    /// - **Vertex deduplication**: Uses HashMap with bitwise float comparison `[x.to_bits(), y.to_bits(), z.to_bits()]`
111    ///   as key. Only exactly identical vertices (bitwise) are merged.
112    /// - **Normal vectors**: Read from STL but ignored (not stored in Model).
113    /// - **Attribute bytes**: Read but ignored (2-byte field after each triangle).
114    ///
115    /// # Examples
116    ///
117    /// ```no_run
118    /// use lib3mf_converters::stl::StlImporter;
119    /// use std::fs::File;
120    ///
121    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
122    /// let file = File::open("cube.stl")?;
123    /// let model = StlImporter::read(file)?;
124    ///
125    /// // Access the imported mesh
126    /// let obj = model.resources.get_object(lib3mf_core::model::resources::ResourceId(1))
127    ///     .expect("STL import creates object with ID 1");
128    /// if let lib3mf_core::model::Geometry::Mesh(mesh) = &obj.geometry {
129    ///     println!("Imported {} vertices, {} triangles",
130    ///         mesh.vertices.len(), mesh.triangles.len());
131    /// }
132    /// # Ok(())
133    /// # }
134    /// ```
135    ///
136    /// [`Model`]: lib3mf_core::model::Model
137    /// [`Lib3mfError::Io`]: lib3mf_core::error::Lib3mfError::Io
138    /// [`Lib3mfError::Validation`]: lib3mf_core::error::Lib3mfError::Validation
139    pub fn read<R: Read>(mut reader: R) -> Result<Model> {
140        // STL Format:
141        // 80 bytes header
142        // 4 bytes triangle info (u32)
143        // Triangles...
144
145        let mut header = [0u8; 80];
146        reader.read_exact(&mut header).map_err(Lib3mfError::Io)?;
147
148        let triangle_count = reader.read_u32::<LittleEndian>().map_err(|_| {
149            Lib3mfError::Validation("Failed to read STL triangle count".to_string())
150        })?;
151
152        let mut mesh = Mesh::default();
153        use std::collections::HashMap;
154        let mut vert_map: HashMap<[u32; 3], u32> = HashMap::new();
155
156        for _ in 0..triangle_count {
157            // Normal (3 floats) - Ignored
158            let _nx = reader.read_f32::<LittleEndian>().map_err(Lib3mfError::Io)?;
159            let _ny = reader.read_f32::<LittleEndian>().map_err(Lib3mfError::Io)?;
160            let _nz = reader.read_f32::<LittleEndian>().map_err(Lib3mfError::Io)?;
161
162            let mut indices = [0u32; 3];
163
164            for index in &mut indices {
165                let x = reader.read_f32::<LittleEndian>().map_err(Lib3mfError::Io)?;
166                let y = reader.read_f32::<LittleEndian>().map_err(Lib3mfError::Io)?;
167                let z = reader.read_f32::<LittleEndian>().map_err(Lib3mfError::Io)?;
168
169                let key = [x.to_bits(), y.to_bits(), z.to_bits()];
170
171                let idx = *vert_map.entry(key).or_insert_with(|| {
172                    let new_idx = mesh.vertices.len() as u32;
173                    mesh.vertices.push(Vertex { x, y, z });
174                    new_idx
175                });
176                *index = idx;
177            }
178
179            let _attr_byte_count = reader.read_u16::<LittleEndian>().map_err(Lib3mfError::Io)?;
180
181            mesh.triangles.push(Triangle {
182                v1: indices[0],
183                v2: indices[1],
184                v3: indices[2],
185                ..Default::default()
186            });
187        }
188
189        let mut model = Model::default();
190        let resource_id = ResourceId(1); // Default ID
191
192        let object = lib3mf_core::model::Object {
193            id: resource_id,
194            object_type: lib3mf_core::model::ObjectType::Model,
195            name: Some("STL Import".to_string()),
196            part_number: None,
197            uuid: None,
198            pid: None,
199            pindex: None,
200            thumbnail: None,
201            geometry: lib3mf_core::model::Geometry::Mesh(mesh),
202        };
203
204        let _ = model.resources.add_object(object);
205
206        model.build.items.push(BuildItem {
207            object_id: resource_id,
208            transform: glam::Mat4::IDENTITY,
209            part_number: None,
210            uuid: None, // Generate one?
211            path: None,
212        });
213
214        Ok(model)
215    }
216}
217
218/// Exports 3MF [`Model`] structures to binary STL files.
219///
220/// The exporter flattens all mesh objects referenced in build items into a single STL file,
221/// applying build item transformations to vertex coordinates.
222///
223/// [`Model`]: lib3mf_core::model::Model
224pub struct StlExporter;
225
226impl StlExporter {
227    /// Writes a 3MF [`Model`] to binary STL format.
228    ///
229    /// # Arguments
230    ///
231    /// * `model` - The 3MF model to export
232    /// * `writer` - Any type implementing [`Write`] to receive STL data
233    ///
234    /// # Returns
235    ///
236    /// `Ok(())` on successful export.
237    ///
238    /// # Errors
239    ///
240    /// Returns [`Lib3mfError::Io`] if any write operation fails.
241    ///
242    /// # Format Details
243    ///
244    /// - **Header**: 80 zero bytes (standard for most STL files)
245    /// - **Normals**: Written as (0, 0, 0) - viewers must compute face normals
246    /// - **Transformations**: Build item transforms are applied to vertex coordinates
247    /// - **Attribute bytes**: Written as 0 (no extended attributes)
248    ///
249    /// # Behavior
250    ///
251    /// - Only mesh objects from `model.build.items` are exported
252    /// - Non-mesh geometries (Components, BooleanShape, etc.) are skipped
253    /// - Each build item's transformation matrix is applied to its mesh vertices
254    /// - All triangles from all build items are combined into a single STL file
255    ///
256    /// # Examples
257    ///
258    /// ```no_run
259    /// use lib3mf_converters::stl::StlExporter;
260    /// use lib3mf_core::model::Model;
261    /// use std::fs::File;
262    ///
263    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
264    /// # let model = Model::default();
265    /// let output = File::create("exported.stl")?;
266    /// StlExporter::write(&model, output)?;
267    /// println!("Model exported successfully");
268    /// # Ok(())
269    /// # }
270    /// ```
271    ///
272    /// [`Model`]: lib3mf_core::model::Model
273    /// [`Lib3mfError::Io`]: lib3mf_core::error::Lib3mfError::Io
274    pub fn write<W: Write>(model: &Model, mut writer: W) -> Result<()> {
275        // 1. Collect all triangles from all build items
276        let mut triangles: Vec<(glam::Vec3, glam::Vec3, glam::Vec3)> = Vec::new(); // v1, v2, v3
277
278        for item in &model.build.items {
279            #[allow(clippy::collapsible_if)]
280            if let Some(object) = model.resources.get_object(item.object_id) {
281                if let lib3mf_core::model::Geometry::Mesh(mesh) = &object.geometry {
282                    let transform = item.transform;
283
284                    for tri in &mesh.triangles {
285                        let v1_local = mesh.vertices[tri.v1 as usize];
286                        let v2_local = mesh.vertices[tri.v2 as usize];
287                        let v3_local = mesh.vertices[tri.v3 as usize];
288
289                        let v1 = transform
290                            .transform_point3(glam::Vec3::new(v1_local.x, v1_local.y, v1_local.z));
291                        let v2 = transform
292                            .transform_point3(glam::Vec3::new(v2_local.x, v2_local.y, v2_local.z));
293                        let v3 = transform
294                            .transform_point3(glam::Vec3::new(v3_local.x, v3_local.y, v3_local.z));
295
296                        triangles.push((v1, v2, v3));
297                    }
298                }
299            }
300        }
301
302        // 2. Write Header (80 bytes)
303        let header = [0u8; 80];
304        writer.write_all(&header).map_err(Lib3mfError::Io)?;
305
306        // 3. Write Count
307        writer
308            .write_u32::<LittleEndian>(triangles.len() as u32)
309            .map_err(Lib3mfError::Io)?;
310
311        // 4. Write Triangles
312        for (v1, v2, v3) in triangles {
313            // Normal (0,0,0) - let viewer calculate
314            writer
315                .write_f32::<LittleEndian>(0.0)
316                .map_err(Lib3mfError::Io)?;
317            writer
318                .write_f32::<LittleEndian>(0.0)
319                .map_err(Lib3mfError::Io)?;
320            writer
321                .write_f32::<LittleEndian>(0.0)
322                .map_err(Lib3mfError::Io)?;
323
324            // v1
325            writer
326                .write_f32::<LittleEndian>(v1.x)
327                .map_err(Lib3mfError::Io)?;
328            writer
329                .write_f32::<LittleEndian>(v1.y)
330                .map_err(Lib3mfError::Io)?;
331            writer
332                .write_f32::<LittleEndian>(v1.z)
333                .map_err(Lib3mfError::Io)?;
334
335            // v2
336            writer
337                .write_f32::<LittleEndian>(v2.x)
338                .map_err(Lib3mfError::Io)?;
339            writer
340                .write_f32::<LittleEndian>(v2.y)
341                .map_err(Lib3mfError::Io)?;
342            writer
343                .write_f32::<LittleEndian>(v2.z)
344                .map_err(Lib3mfError::Io)?;
345
346            // v3
347            writer
348                .write_f32::<LittleEndian>(v3.x)
349                .map_err(Lib3mfError::Io)?;
350            writer
351                .write_f32::<LittleEndian>(v3.y)
352                .map_err(Lib3mfError::Io)?;
353            writer
354                .write_f32::<LittleEndian>(v3.z)
355                .map_err(Lib3mfError::Io)?;
356
357            // Attribute byte count (0)
358            writer
359                .write_u16::<LittleEndian>(0)
360                .map_err(Lib3mfError::Io)?;
361        }
362
363        Ok(())
364    }
365
366    /// Writes a 3MF [`Model`] to binary STL format with support for multi-part 3MF files.
367    ///
368    /// This method extends [`write`] by recursively resolving component references and external
369    /// model parts using a [`PartResolver`]. This is necessary for 3MF files with the Production
370    /// Extension that reference objects from external model parts.
371    ///
372    /// # Arguments
373    ///
374    /// * `model` - The root 3MF model to export
375    /// * `resolver` - A [`PartResolver`] for loading external model parts from the 3MF archive
376    /// * `writer` - Any type implementing [`Write`] to receive STL data
377    ///
378    /// # Returns
379    ///
380    /// `Ok(())` on successful export.
381    ///
382    /// # Errors
383    ///
384    /// Returns [`Lib3mfError::Io`] if any write operation fails.
385    ///
386    /// Returns errors from the resolver if external parts cannot be loaded.
387    ///
388    /// # Behavior
389    ///
390    /// - Recursively resolves component hierarchies using the PartResolver
391    /// - Follows external references via component `path` attributes
392    /// - Applies accumulated transformations through the component tree
393    /// - Flattens all resolved meshes into a single STL file
394    ///
395    /// # Examples
396    ///
397    /// ```no_run
398    /// use lib3mf_converters::stl::StlExporter;
399    /// use lib3mf_core::archive::ZipArchiver;
400    /// use lib3mf_core::model::resolver::PartResolver;
401    /// use std::fs::File;
402    ///
403    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
404    /// # let model = lib3mf_core::model::Model::default();
405    /// let archive_file = File::open("multipart.3mf")?;
406    /// let mut archiver = ZipArchiver::new(archive_file)?;
407    /// let resolver = PartResolver::new(&mut archiver, model.clone());
408    ///
409    /// let output = File::create("output.stl")?;
410    /// StlExporter::write_with_resolver(&model, resolver, output)?;
411    /// # Ok(())
412    /// # }
413    /// ```
414    ///
415    /// [`write`]: StlExporter::write
416    /// [`Model`]: lib3mf_core::model::Model
417    /// [`PartResolver`]: lib3mf_core::model::resolver::PartResolver
418    /// [`Lib3mfError::Io`]: lib3mf_core::error::Lib3mfError::Io
419    pub fn write_with_resolver<W: Write, A: lib3mf_core::archive::ArchiveReader>(
420        model: &Model,
421        mut resolver: lib3mf_core::model::resolver::PartResolver<A>,
422        mut writer: W,
423    ) -> Result<()> {
424        // 1. Collect all triangles from all build items (recursively)
425        let mut triangles: Vec<(glam::Vec3, glam::Vec3, glam::Vec3)> = Vec::new();
426
427        for item in &model.build.items {
428            collect_triangles(
429                &mut resolver,
430                item.object_id,
431                item.transform,
432                None, // Start with root path (None)
433                &mut triangles,
434            )?;
435        }
436
437        // 2. Write Header (80 bytes)
438        let header = [0u8; 80];
439        writer.write_all(&header).map_err(Lib3mfError::Io)?;
440
441        // 3. Write Count
442        writer
443            .write_u32::<LittleEndian>(triangles.len() as u32)
444            .map_err(Lib3mfError::Io)?;
445
446        // 4. Write Triangles
447        for (v1, v2, v3) in triangles {
448            // Normal (0,0,0) - let viewer calculate
449            writer
450                .write_f32::<LittleEndian>(0.0)
451                .map_err(Lib3mfError::Io)?;
452            writer
453                .write_f32::<LittleEndian>(0.0)
454                .map_err(Lib3mfError::Io)?;
455            writer
456                .write_f32::<LittleEndian>(0.0)
457                .map_err(Lib3mfError::Io)?;
458
459            // v1
460            writer
461                .write_f32::<LittleEndian>(v1.x)
462                .map_err(Lib3mfError::Io)?;
463            writer
464                .write_f32::<LittleEndian>(v1.y)
465                .map_err(Lib3mfError::Io)?;
466            writer
467                .write_f32::<LittleEndian>(v1.z)
468                .map_err(Lib3mfError::Io)?;
469
470            // v2
471            writer
472                .write_f32::<LittleEndian>(v2.x)
473                .map_err(Lib3mfError::Io)?;
474            writer
475                .write_f32::<LittleEndian>(v2.y)
476                .map_err(Lib3mfError::Io)?;
477            writer
478                .write_f32::<LittleEndian>(v2.z)
479                .map_err(Lib3mfError::Io)?;
480
481            // v3
482            writer
483                .write_f32::<LittleEndian>(v3.x)
484                .map_err(Lib3mfError::Io)?;
485            writer
486                .write_f32::<LittleEndian>(v3.y)
487                .map_err(Lib3mfError::Io)?;
488            writer
489                .write_f32::<LittleEndian>(v3.z)
490                .map_err(Lib3mfError::Io)?;
491
492            // Attribute byte count (0)
493            writer
494                .write_u16::<LittleEndian>(0)
495                .map_err(Lib3mfError::Io)?;
496        }
497
498        Ok(())
499    }
500}
501
502fn collect_triangles<A: lib3mf_core::archive::ArchiveReader>(
503    resolver: &mut lib3mf_core::model::resolver::PartResolver<A>,
504    object_id: ResourceId,
505    transform: glam::Mat4,
506    path: Option<&str>,
507    triangles: &mut Vec<(glam::Vec3, glam::Vec3, glam::Vec3)>,
508) -> Result<()> {
509    // Resolve geometry
510    // Note: We need to clone the geometry or handle the borrow of resolver carefully.
511    // resolving returns a reference to Model and Object, which borrows from resolver.
512    // We can't keep that borrow while recursing (mutably borrowing resolver).
513    // So we need to clone the relevant data (Geometry) or restructure.
514    // Cloning Geometry (which contains Mesh) might be expensive but safe.
515    // OR: resolve_object returns reference, we inspect it, then drop reference before recursing.
516
517    let geometry = {
518        let res = resolver.resolve_object(object_id, path)?;
519        if let Some((_, obj)) = res {
520            Some(obj.geometry.clone()) // Cloning geometry to release borrow
521        } else {
522            None
523        }
524    };
525
526    if let Some(geo) = geometry {
527        match geo {
528            lib3mf_core::model::Geometry::Mesh(mesh) => {
529                for tri in &mesh.triangles {
530                    let v1_local = mesh.vertices[tri.v1 as usize];
531                    let v2_local = mesh.vertices[tri.v2 as usize];
532                    let v3_local = mesh.vertices[tri.v3 as usize];
533
534                    let v1 = transform
535                        .transform_point3(glam::Vec3::new(v1_local.x, v1_local.y, v1_local.z));
536                    let v2 = transform
537                        .transform_point3(glam::Vec3::new(v2_local.x, v2_local.y, v2_local.z));
538                    let v3 = transform
539                        .transform_point3(glam::Vec3::new(v3_local.x, v3_local.y, v3_local.z));
540
541                    triangles.push((v1, v2, v3));
542                }
543            }
544            lib3mf_core::model::Geometry::Components(comps) => {
545                for comp in comps.components {
546                    let new_transform = transform * comp.transform;
547                    // Path handling: If component references a different path/part, update it?
548                    // Currently lib3mf-core resolver handles relative paths if we pass them.
549                    // The component struct has a 'path' field? (Checking core definition...)
550                    // Assuming Component has 'path' field which is Option<String>
551                    // Wait, I need to check lib3mf_core::model::Component definition.
552                    // Assuming it does based on previous experience, but let's be safe.
553                    // Actually, looking at `print_tree` in commands.rs, it uses `comp.path`.
554
555                    let next_path_store = comp.path.clone();
556                    // If component has path, it overrides/is relative to current?
557                    // Usually in 3MF, if path represents a model part.
558                    // resolver logic: "if path is provided, look there".
559
560                    let next_path = next_path_store.as_deref().or(path);
561
562                    collect_triangles(
563                        resolver,
564                        comp.object_id,
565                        new_transform,
566                        next_path,
567                        triangles,
568                    )?;
569                }
570            }
571            _ => {}
572        }
573    }
574
575    Ok(())
576}