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            printable: None,
213        });
214
215        Ok(model)
216    }
217}
218
219/// Exports 3MF [`Model`] structures to binary STL files.
220///
221/// The exporter flattens all mesh objects referenced in build items into a single STL file,
222/// applying build item transformations to vertex coordinates.
223///
224/// [`Model`]: lib3mf_core::model::Model
225pub struct StlExporter;
226
227impl StlExporter {
228    /// Writes a 3MF [`Model`] to binary STL format.
229    ///
230    /// # Arguments
231    ///
232    /// * `model` - The 3MF model to export
233    /// * `writer` - Any type implementing [`Write`] to receive STL data
234    ///
235    /// # Returns
236    ///
237    /// `Ok(())` on successful export.
238    ///
239    /// # Errors
240    ///
241    /// Returns [`Lib3mfError::Io`] if any write operation fails.
242    ///
243    /// # Format Details
244    ///
245    /// - **Header**: 80 zero bytes (standard for most STL files)
246    /// - **Normals**: Written as (0, 0, 0) - viewers must compute face normals
247    /// - **Transformations**: Build item transforms are applied to vertex coordinates
248    /// - **Attribute bytes**: Written as 0 (no extended attributes)
249    ///
250    /// # Behavior
251    ///
252    /// - Only mesh objects from `model.build.items` are exported
253    /// - Non-mesh geometries (Components, BooleanShape, etc.) are skipped
254    /// - Each build item's transformation matrix is applied to its mesh vertices
255    /// - All triangles from all build items are combined into a single STL file
256    ///
257    /// # Examples
258    ///
259    /// ```no_run
260    /// use lib3mf_converters::stl::StlExporter;
261    /// use lib3mf_core::model::Model;
262    /// use std::fs::File;
263    ///
264    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
265    /// # let model = Model::default();
266    /// let output = File::create("exported.stl")?;
267    /// StlExporter::write(&model, output)?;
268    /// println!("Model exported successfully");
269    /// # Ok(())
270    /// # }
271    /// ```
272    ///
273    /// [`Model`]: lib3mf_core::model::Model
274    /// [`Lib3mfError::Io`]: lib3mf_core::error::Lib3mfError::Io
275    pub fn write<W: Write>(model: &Model, mut writer: W) -> Result<()> {
276        // 1. Collect all triangles from all build items
277        let mut triangles: Vec<(glam::Vec3, glam::Vec3, glam::Vec3)> = Vec::new(); // v1, v2, v3
278
279        for item in &model.build.items {
280            #[allow(clippy::collapsible_if)]
281            if let Some(object) = model.resources.get_object(item.object_id) {
282                if let lib3mf_core::model::Geometry::Mesh(mesh) = &object.geometry {
283                    let transform = item.transform;
284
285                    for tri in &mesh.triangles {
286                        let v1_local = mesh.vertices[tri.v1 as usize];
287                        let v2_local = mesh.vertices[tri.v2 as usize];
288                        let v3_local = mesh.vertices[tri.v3 as usize];
289
290                        let v1 = transform
291                            .transform_point3(glam::Vec3::new(v1_local.x, v1_local.y, v1_local.z));
292                        let v2 = transform
293                            .transform_point3(glam::Vec3::new(v2_local.x, v2_local.y, v2_local.z));
294                        let v3 = transform
295                            .transform_point3(glam::Vec3::new(v3_local.x, v3_local.y, v3_local.z));
296
297                        triangles.push((v1, v2, v3));
298                    }
299                }
300            }
301        }
302
303        // 2. Write Header (80 bytes)
304        let header = [0u8; 80];
305        writer.write_all(&header).map_err(Lib3mfError::Io)?;
306
307        // 3. Write Count
308        writer
309            .write_u32::<LittleEndian>(triangles.len() as u32)
310            .map_err(Lib3mfError::Io)?;
311
312        // 4. Write Triangles
313        for (v1, v2, v3) in triangles {
314            // Normal (0,0,0) - let viewer calculate
315            writer
316                .write_f32::<LittleEndian>(0.0)
317                .map_err(Lib3mfError::Io)?;
318            writer
319                .write_f32::<LittleEndian>(0.0)
320                .map_err(Lib3mfError::Io)?;
321            writer
322                .write_f32::<LittleEndian>(0.0)
323                .map_err(Lib3mfError::Io)?;
324
325            // v1
326            writer
327                .write_f32::<LittleEndian>(v1.x)
328                .map_err(Lib3mfError::Io)?;
329            writer
330                .write_f32::<LittleEndian>(v1.y)
331                .map_err(Lib3mfError::Io)?;
332            writer
333                .write_f32::<LittleEndian>(v1.z)
334                .map_err(Lib3mfError::Io)?;
335
336            // v2
337            writer
338                .write_f32::<LittleEndian>(v2.x)
339                .map_err(Lib3mfError::Io)?;
340            writer
341                .write_f32::<LittleEndian>(v2.y)
342                .map_err(Lib3mfError::Io)?;
343            writer
344                .write_f32::<LittleEndian>(v2.z)
345                .map_err(Lib3mfError::Io)?;
346
347            // v3
348            writer
349                .write_f32::<LittleEndian>(v3.x)
350                .map_err(Lib3mfError::Io)?;
351            writer
352                .write_f32::<LittleEndian>(v3.y)
353                .map_err(Lib3mfError::Io)?;
354            writer
355                .write_f32::<LittleEndian>(v3.z)
356                .map_err(Lib3mfError::Io)?;
357
358            // Attribute byte count (0)
359            writer
360                .write_u16::<LittleEndian>(0)
361                .map_err(Lib3mfError::Io)?;
362        }
363
364        Ok(())
365    }
366
367    /// Writes a 3MF [`Model`] to binary STL format with support for multi-part 3MF files.
368    ///
369    /// This method extends [`write`] by recursively resolving component references and external
370    /// model parts using a [`PartResolver`]. This is necessary for 3MF files with the Production
371    /// Extension that reference objects from external model parts.
372    ///
373    /// # Arguments
374    ///
375    /// * `model` - The root 3MF model to export
376    /// * `resolver` - A [`PartResolver`] for loading external model parts from the 3MF archive
377    /// * `writer` - Any type implementing [`Write`] to receive STL data
378    ///
379    /// # Returns
380    ///
381    /// `Ok(())` on successful export.
382    ///
383    /// # Errors
384    ///
385    /// Returns [`Lib3mfError::Io`] if any write operation fails.
386    ///
387    /// Returns errors from the resolver if external parts cannot be loaded.
388    ///
389    /// # Behavior
390    ///
391    /// - Recursively resolves component hierarchies using the PartResolver
392    /// - Follows external references via component `path` attributes
393    /// - Applies accumulated transformations through the component tree
394    /// - Flattens all resolved meshes into a single STL file
395    ///
396    /// # Examples
397    ///
398    /// ```no_run
399    /// use lib3mf_converters::stl::StlExporter;
400    /// use lib3mf_core::archive::ZipArchiver;
401    /// use lib3mf_core::model::resolver::PartResolver;
402    /// use std::fs::File;
403    ///
404    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
405    /// # let model = lib3mf_core::model::Model::default();
406    /// let archive_file = File::open("multipart.3mf")?;
407    /// let mut archiver = ZipArchiver::new(archive_file)?;
408    /// let resolver = PartResolver::new(&mut archiver, model.clone());
409    ///
410    /// let output = File::create("output.stl")?;
411    /// StlExporter::write_with_resolver(&model, resolver, output)?;
412    /// # Ok(())
413    /// # }
414    /// ```
415    ///
416    /// [`write`]: StlExporter::write
417    /// [`Model`]: lib3mf_core::model::Model
418    /// [`PartResolver`]: lib3mf_core::model::resolver::PartResolver
419    /// [`Lib3mfError::Io`]: lib3mf_core::error::Lib3mfError::Io
420    pub fn write_with_resolver<W: Write, A: lib3mf_core::archive::ArchiveReader>(
421        model: &Model,
422        mut resolver: lib3mf_core::model::resolver::PartResolver<A>,
423        mut writer: W,
424    ) -> Result<()> {
425        // 1. Collect all triangles from all build items (recursively)
426        let mut triangles: Vec<(glam::Vec3, glam::Vec3, glam::Vec3)> = Vec::new();
427
428        for item in &model.build.items {
429            collect_triangles(
430                &mut resolver,
431                item.object_id,
432                item.transform,
433                None, // Start with root path (None)
434                &mut triangles,
435            )?;
436        }
437
438        // 2. Write Header (80 bytes)
439        let header = [0u8; 80];
440        writer.write_all(&header).map_err(Lib3mfError::Io)?;
441
442        // 3. Write Count
443        writer
444            .write_u32::<LittleEndian>(triangles.len() as u32)
445            .map_err(Lib3mfError::Io)?;
446
447        // 4. Write Triangles
448        for (v1, v2, v3) in triangles {
449            // Normal (0,0,0) - let viewer calculate
450            writer
451                .write_f32::<LittleEndian>(0.0)
452                .map_err(Lib3mfError::Io)?;
453            writer
454                .write_f32::<LittleEndian>(0.0)
455                .map_err(Lib3mfError::Io)?;
456            writer
457                .write_f32::<LittleEndian>(0.0)
458                .map_err(Lib3mfError::Io)?;
459
460            // v1
461            writer
462                .write_f32::<LittleEndian>(v1.x)
463                .map_err(Lib3mfError::Io)?;
464            writer
465                .write_f32::<LittleEndian>(v1.y)
466                .map_err(Lib3mfError::Io)?;
467            writer
468                .write_f32::<LittleEndian>(v1.z)
469                .map_err(Lib3mfError::Io)?;
470
471            // v2
472            writer
473                .write_f32::<LittleEndian>(v2.x)
474                .map_err(Lib3mfError::Io)?;
475            writer
476                .write_f32::<LittleEndian>(v2.y)
477                .map_err(Lib3mfError::Io)?;
478            writer
479                .write_f32::<LittleEndian>(v2.z)
480                .map_err(Lib3mfError::Io)?;
481
482            // v3
483            writer
484                .write_f32::<LittleEndian>(v3.x)
485                .map_err(Lib3mfError::Io)?;
486            writer
487                .write_f32::<LittleEndian>(v3.y)
488                .map_err(Lib3mfError::Io)?;
489            writer
490                .write_f32::<LittleEndian>(v3.z)
491                .map_err(Lib3mfError::Io)?;
492
493            // Attribute byte count (0)
494            writer
495                .write_u16::<LittleEndian>(0)
496                .map_err(Lib3mfError::Io)?;
497        }
498
499        Ok(())
500    }
501}
502
503fn collect_triangles<A: lib3mf_core::archive::ArchiveReader>(
504    resolver: &mut lib3mf_core::model::resolver::PartResolver<A>,
505    object_id: ResourceId,
506    transform: glam::Mat4,
507    path: Option<&str>,
508    triangles: &mut Vec<(glam::Vec3, glam::Vec3, glam::Vec3)>,
509) -> Result<()> {
510    // Resolve geometry
511    // Note: We need to clone the geometry or handle the borrow of resolver carefully.
512    // resolving returns a reference to Model and Object, which borrows from resolver.
513    // We can't keep that borrow while recursing (mutably borrowing resolver).
514    // So we need to clone the relevant data (Geometry) or restructure.
515    // Cloning Geometry (which contains Mesh) might be expensive but safe.
516    // OR: resolve_object returns reference, we inspect it, then drop reference before recursing.
517
518    let geometry = {
519        let res = resolver.resolve_object(object_id, path)?;
520        if let Some((_, obj)) = res {
521            Some(obj.geometry.clone()) // Cloning geometry to release borrow
522        } else {
523            None
524        }
525    };
526
527    if let Some(geo) = geometry {
528        match geo {
529            lib3mf_core::model::Geometry::Mesh(mesh) => {
530                for tri in &mesh.triangles {
531                    let v1_local = mesh.vertices[tri.v1 as usize];
532                    let v2_local = mesh.vertices[tri.v2 as usize];
533                    let v3_local = mesh.vertices[tri.v3 as usize];
534
535                    let v1 = transform
536                        .transform_point3(glam::Vec3::new(v1_local.x, v1_local.y, v1_local.z));
537                    let v2 = transform
538                        .transform_point3(glam::Vec3::new(v2_local.x, v2_local.y, v2_local.z));
539                    let v3 = transform
540                        .transform_point3(glam::Vec3::new(v3_local.x, v3_local.y, v3_local.z));
541
542                    triangles.push((v1, v2, v3));
543                }
544            }
545            lib3mf_core::model::Geometry::Components(comps) => {
546                for comp in comps.components {
547                    let new_transform = transform * comp.transform;
548                    // Path handling: If component references a different path/part, update it?
549                    // Currently lib3mf-core resolver handles relative paths if we pass them.
550                    // The component struct has a 'path' field? (Checking core definition...)
551                    // Assuming Component has 'path' field which is Option<String>
552                    // Wait, I need to check lib3mf_core::model::Component definition.
553                    // Assuming it does based on previous experience, but let's be safe.
554                    // Actually, looking at `print_tree` in commands.rs, it uses `comp.path`.
555
556                    let next_path_store = comp.path.clone();
557                    // If component has path, it overrides/is relative to current?
558                    // Usually in 3MF, if path represents a model part.
559                    // resolver logic: "if path is provided, look there".
560
561                    let next_path = next_path_store.as_deref().or(path);
562
563                    collect_triangles(
564                        resolver,
565                        comp.object_id,
566                        new_transform,
567                        next_path,
568                        triangles,
569                    )?;
570                }
571            }
572            _ => {}
573        }
574    }
575
576    Ok(())
577}