diff --git a/Cargo.lock b/Cargo.lock index d0c2a1ed6..f9e38d25b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,6 +183,12 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8b41881888cc08af32d47ac4edd52bc7fa27fef774be47a92443756451304" + [[package]] name = "byteorder" version = "1.4.3" @@ -727,8 +733,10 @@ dependencies = [ name = "fj-export" version = "0.6.0" dependencies = [ + "anyhow", "fj-interop", "fj-math", + "stl", "threemf", ] @@ -951,7 +959,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" dependencies = [ - "byteorder", + "byteorder 1.4.3", ] [[package]] @@ -2483,6 +2491,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stl" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "466e72b3a9258f51f0562a01f2aea3717fb71d9997f4050c65c251a623926e12" +dependencies = [ + "byteorder 0.4.2", +] + [[package]] name = "strsim" version = "0.10.0" @@ -3261,7 +3278,7 @@ version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" dependencies = [ - "byteorder", + "byteorder 1.4.3", "bzip2", "crc32fast", "flate2", diff --git a/README.md b/README.md index 71e309f89..6b85af68d 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,9 @@ As of this writing, Fornjot runs on Linux, Windows, and macOS. The project is pr Short- to mid-term, the plan is to add support for the web platform, so Fornjot can run in browsers. Long-term, the plan is to additionally support the major mobile platforms. -### Export to 3MF +### Export to 3MF & STL -Exporting models to the [3D Manufacturing Format](https://en.wikipedia.org/wiki/3D_Manufacturing_Format) (3MF), which is used in 3D printing, is supported. +Exporting models to both the [3D Manufacturing Format](https://en.wikipedia.org/wiki/3D_Manufacturing_Format) (3MF), which is used in 3D printing, and STL is supported. ## Usage @@ -126,12 +126,14 @@ So far, the host application is not published on [crates.io](https://crates.io/) ### Exporting models -To export a model to a 3MF file, run: +To export a model to a file, run: ``` sh cargo run -- -m spacer --export spacer.3mf ``` +The file type is based on the supplied extension. Both 3MF and STL are supported. + ### Model parameters Some models have parameters that can be overridden. For example, to override the inner and outer radii of the spacer model: diff --git a/crates/fj-export/Cargo.toml b/crates/fj-export/Cargo.toml index 0ea7ff4b9..dd85e88c9 100644 --- a/crates/fj-export/Cargo.toml +++ b/crates/fj-export/Cargo.toml @@ -13,7 +13,9 @@ categories = ["encoding", "mathematics", "rendering"] [dependencies] +anyhow = "1.0.57" threemf = "0.3.0" +stl = "0.2.1" [dependencies.fj-interop] version = "0.6.0" diff --git a/crates/fj-export/src/lib.rs b/crates/fj-export/src/lib.rs index 2165100e8..130fb0a2a 100644 --- a/crates/fj-export/src/lib.rs +++ b/crates/fj-export/src/lib.rs @@ -14,16 +14,35 @@ #![deny(missing_docs)] -use std::path::Path; +use std::{fs::File, path::Path}; + +use anyhow::{anyhow, Result}; use fj_interop::mesh::Mesh; -use fj_math::Point; +use fj_math::{Point, Triangle, Vector}; -/// Export the provided mesh to the file at the given path +/// Export the provided mesh to the file at the given path. +/// +/// This function will create a file if it does not exist, and will truncate it if it does. /// -/// Currently only 3MF is supported as an export format. The file extension of -/// the provided path is ignored. -pub fn export(mesh: &Mesh>, path: &Path) -> Result<(), Error> { +/// Currently 3MF & STL file types are supported. The case insensitive file extension of +/// the provided path is used to switch between supported types. +pub fn export(mesh: &Mesh>, path: &Path) -> Result<()> { + match path.extension() { + Some(extension) if extension.to_ascii_uppercase() == "3MF" => { + export_3mf(mesh, path) + } + Some(extension) if extension.to_ascii_uppercase() == "STL" => { + export_stl(mesh, path) + } + Some(extension) => { + Err(anyhow!("Extension not recognised, got {:?}", extension)) + } + None => Err(anyhow!("No extension specified")), + } +} + +fn export_3mf(mesh: &Mesh>, path: &Path) -> Result<()> { let vertices = mesh.vertices().map(|vertex| vertex.into()).collect(); let indices: Vec<_> = mesh.indices().collect(); @@ -48,4 +67,57 @@ pub fn export(mesh: &Mesh>, path: &Path) -> Result<(), Error> { Ok(()) } -pub use threemf::Error; +fn export_stl(mesh: &Mesh>, path: &Path) -> Result<()> { + let points = mesh + .triangles() + .map(|triangle| triangle.points) + .collect::>(); + + let vertices = points.iter().map(|points| { + points.map(|point| { + [point.x.into_f32(), point.y.into_f32(), point.z.into_f32()] + }) + }); + + let normals = points + .iter() + .map(|&points| points.into()) + .map(|triangle: Triangle<3>| triangle.to_parry().normal()) + .collect::>>() + .ok_or_else(|| anyhow!("Unable to compute normal"))?; + + let normals = normals.iter().map(|vector| vector.into_inner().into()).map( + |vector: Vector<3>| { + [ + vector.x.into_f32(), + vector.y.into_f32(), + vector.z.into_f32(), + ] + }, + ); + + let triangles = vertices + .zip(normals) + .map(|([v1, v2, v3], normal)| stl::Triangle { + normal, + v1, + v2, + v3, + attr_byte_count: 0, + }) + .collect::>(); + + let mut file = File::create(path)?; + + let binary_stl_file = stl::BinaryStlFile { + header: stl::BinaryStlHeader { + header: [0u8; 80], + num_triangles: triangles.len().try_into()?, + }, + triangles, + }; + + stl::write_stl(&mut file, &binary_stl_file)?; + + Ok(()) +}