1use crate::primitives::mesh::{MeshCollection, Normal, Triangle, TriangleMesh};
86use anyhow::{Error, Ok, Result};
87use regex::Regex;
88use std::io::Write;
89
90const STL_REGEX: &str = r"(?x)
91 facet\snormal\s+
92 (-?\d*\.?\d+(?:[eE]-?\d+)?)\s+
93 (-?\d*\.?\d+(?:[eE]-?\d+)?)\s+
94 (-?\d*\.?\d+(?:[eE]-?\d+)?)[\s\S]*?
95 vertex\s+
96 (-?\d*\.?\d+(?:[eE]-?\d+)?)\s+
97 (-?\d*\.?\d+(?:[eE]-?\d+)?)\s+
98 (-?\d*\.?\d+(?:[eE]-?\d+)?)[\s\S]*?
99 vertex\s+
100 (-?\d*\.?\d+(?:[eE]-?\d+)?)\s+
101 (-?\d*\.?\d+(?:[eE]-?\d+)?)\s+
102 (-?\d*\.?\d+(?:[eE]-?\d+)?)[\s\S]*?
103 vertex\s+
104 (-?\d*\.?\d+(?:[eE]-?\d+)?)\s+
105 (-?\d*\.?\d+(?:[eE]-?\d+)?)\s+
106 (-?\d*\.?\d+(?:[eE]-?\d+)?).*
107";
108
109pub struct Facet<'a> {
111 normal: &'a Normal,
113 vertices: &'a Triangle,
115}
116
117impl Facet<'_> {
118 fn to_stl(&self) -> String {
120 format!(
121 "facet normal {} {} {}\nouter loop\nvertex {} {} {}\nvertex {} {} {}\nvertex {} {} {}\nendloop\nendfacet\n",
122 self.normal[0],
123 self.normal[1],
124 self.normal[2],
125 self.vertices.p0()[0],
126 self.vertices.p0()[1],
127 self.vertices.p0()[2],
128 self.vertices.p1()[0],
129 self.vertices.p1()[1],
130 self.vertices.p1()[2],
131 self.vertices.p2()[0],
132 self.vertices.p2()[1],
133 self.vertices.p2()[2],
134 )
135 }
136}
137
138impl MeshCollection {
139 pub fn write_stl(&self, path: &str) -> Result<(), Error> {
141 let path_buf = std::path::PathBuf::from(path);
142 std::fs::create_dir_all(path_buf.parent().expect("Failed to get parent directory"))
143 .expect("Failed to create directory");
144 let mut file = std::fs::File::create(path)?;
145 write!(file, "{}", self.to_stl())?;
146 Ok(())
147 }
148
149 pub fn to_stl(&self) -> String {
150 let mut stl = String::new();
151 for mesh in self.meshes.iter() {
152 stl.push_str(&format!(
153 "solid\n{}endsolid\n",
154 mesh.triangles
155 .iter()
156 .zip(mesh.normals().iter())
157 .map(|(tri, norm)| Facet {
158 normal: norm,
159 vertices: tri,
160 })
161 .map(|facet| facet.to_stl())
162 .collect::<String>()
163 ));
164 }
165 stl
166 }
167
168 pub fn from_stl(stl_str: &str) -> Result<Self> {
170 let re = Regex::new(STL_REGEX).unwrap();
172 let triangle_norms = re
174 .captures_iter(stl_str)
175 .map(|caps| {
176 let normal = [
177 caps[1].parse::<f32>()?,
178 caps[2].parse::<f32>()?,
179 caps[3].parse::<f32>()?,
180 ];
181 let vertices = [
182 [
183 caps[4].parse::<f32>()?,
184 caps[5].parse::<f32>()?,
185 caps[6].parse::<f32>()?,
186 ],
187 [
188 caps[7].parse::<f32>()?,
189 caps[8].parse::<f32>()?,
190 caps[9].parse::<f32>()?,
191 ],
192 [
193 caps[10].parse::<f32>()?,
194 caps[11].parse::<f32>()?,
195 caps[12].parse::<f32>()?,
196 ],
197 ];
198 Ok((Triangle::new(vertices), normal))
199 })
200 .filter_map(|tri_norm: Result<(Triangle, Normal), anyhow::Error>| tri_norm.ok())
201 .unzip();
202 Ok(TriangleMesh {
203 triangles: triangle_norms.0,
204 normals: triangle_norms.1,
205 }
206 .into())
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::{csg::prelude::*, primitives::bounding::BoundingBox, primitives::prelude::*};
214 use backend_macro::with_backend;
215 use rstest::{fixture, rstest};
216 use std::fs;
217 use std::path::Path;
218
219 const RESOLUTION: usize = 40;
221
222 #[fixture]
223 fn bounds() -> BoundingBox<3> {
224 BoundingBox::new([-10.0, -10.0, -10.0], [10.0, 10.0, 10.0])
225 }
226
227 #[with_backend]
228 #[rstest]
229 #[case("sphere")]
230 #[case("torus")]
231 fn stl_render(#[case] filename: &str, bounds: BoundingBox<3>) {
233 let field = match filename {
234 "sphere" => Field3D::<Backend>::sphere(1.0, device()),
235 "torus" => Field3D::<Backend>::torus(1.0, 2.0, device()),
236 _ => panic!("Invalid filename"),
237 };
238
239 let params = MarchingCubesParams {
241 region: field.into_isosurface(0.0).region(),
242 bounds,
243 resolution: (RESOLUTION, RESOLUTION, RESOLUTION),
244 algebra: Algebra::<Backend>::default(),
245 };
246 let mesh: MeshCollection = marching_cubes::<Backend>(¶ms, &device()).into();
247
248 let stl_data = mesh.to_stl();
250 let mut path_buf = std::path::PathBuf::from("./target/test_renderings/");
251 path_buf.push(filename);
252 path_buf.set_extension("stl");
253 std::fs::create_dir_all(path_buf.parent().expect("Failed to get parent directory"))
254 .expect("Failed to create directory");
255 std::fs::write(path_buf, stl_data).expect("Failed to write STL file");
256 }
257
258 #[test]
259 fn test_facet_mesh_to_stl() {
261 let normal = [0.0, 0.0, 1.0];
262 let triangle = Triangle::new([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
263 let facet = Facet {
264 normal: &normal,
265 vertices: &triangle,
266 };
267
268 let expected_stl = "facet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\n";
269 assert_eq!(facet.to_stl(), expected_stl);
270
271 let mesh: MeshCollection = TriangleMesh::new(vec![triangle]).into();
273 let expected_stl = "solid\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid\n";
274 assert_eq!(mesh.to_stl(), expected_stl);
275 }
276
277 #[test]
278 fn test_write_stl() {
280 let vertices = Triangle::new([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
281 let mesh: MeshCollection = TriangleMesh::new(vec![vertices]).into();
282
283 let filename = "./target/test_mesh.stl";
284 mesh.write_stl(filename).expect("Failed to write STL file");
285
286 assert!(Path::new(filename).exists(), "STL file was not created");
287
288 let expected_stl = "solid\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid\n";
289 let written_stl = fs::read_to_string(filename).expect("Failed to read STL file");
290 assert_eq!(written_stl, expected_stl);
291 }
292
293 #[test]
294 fn test_empty_triangle_mesh_stl() {
296 let mesh: MeshCollection = TriangleMesh::new(vec![]).into();
297
298 let expected_stl = "solid\nendsolid\n";
299 assert_eq!(mesh.to_stl(), expected_stl);
300 }
301
302 #[test]
303 fn test_read_stl() {
305 let stl_content = r#"
307 solid cube
308 facet normal 0 0 1
309 outer loop
310 vertex 0 0 0
311 vertex 1 0 0
312 vertex 1 1 0
313 endloop
314 endfacet
315 facet normal 0 1 0
316 outer loop
317 vertex 0 1 0
318 vertex 1 1 0
319 vertex 0 1 1
320 endloop
321 endfacet
322 endsolid cube
323 "#;
324
325 let mesh = MeshCollection::from_stl(stl_content).expect("Failed to read STL file");
326 assert_eq!(mesh.num_triangles(), 2);
327 assert_eq!(mesh.num_vertices(), 6);
328
329 let stl_data = mesh.to_stl();
331 let mesh_round_trip = MeshCollection::from_stl(&stl_data).expect("Failed to read STL file");
332 assert_eq!(mesh, mesh_round_trip);
333 }
334}