1use std::io::{self, Write};
16
17use geop_core_math::{primitives::TriangleFace, scalars::Scalar, vector::Vector3};
18use geop_core_topology::FaceId;
19
20use crate::RasterizedModel;
21
22#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct StlTriangle {
26 pub normal: [f32; 3],
27 pub corners: [[f32; 3]; 3],
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum StlFormat {
33 Binary,
35 Ascii,
37}
38
39fn f32s<S: Scalar>(v: &Vector3<S>) -> [f32; 3] {
40 [
41 v[0].to_f64() as f32,
42 v[1].to_f64() as f32,
43 v[2].to_f64() as f32,
44 ]
45}
46
47fn outward<S: Scalar>(t: &TriangleFace<S>) -> StlTriangle {
50 let flat = f32s(&t.normal);
51 let surface = t.vertex_normals.map(|ns| {
52 ns.iter().map(f32s).fold([0.0; 3], |acc, n| {
53 [acc[0] + n[0], acc[1] + n[1], acc[2] + n[2]]
54 })
55 });
56 let agrees = surface.is_none_or(|s| flat[0] * s[0] + flat[1] * s[1] + flat[2] * s[2] >= 0.0);
57 let [a, b, c] = [f32s(&t.a), f32s(&t.b), f32s(&t.c)];
58 if agrees {
59 StlTriangle {
60 normal: flat,
61 corners: [a, b, c],
62 }
63 } else {
64 StlTriangle {
65 normal: flat.map(|x| -x),
66 corners: [a, c, b],
67 }
68 }
69}
70
71pub fn stl_triangles<S: Scalar>(raster: &RasterizedModel<S>, faces: &[FaceId]) -> Vec<StlTriangle> {
73 faces
74 .iter()
75 .filter_map(|face| raster.faces.get(face))
76 .flatten()
77 .map(outward)
78 .collect()
79}
80
81pub fn write_stl(
84 triangles: &[StlTriangle],
85 name: &str,
86 format: StlFormat,
87 out: &mut impl Write,
88) -> io::Result<()> {
89 match format {
90 StlFormat::Binary => {
91 let mut header = [b' '; 80];
94 let text = format!("geop {name}");
95 let len = text.len().min(80);
96 header[..len].copy_from_slice(&text.as_bytes()[..len]);
97 out.write_all(&header)?;
98 let count = u32::try_from(triangles.len())
99 .map_err(|_| io::Error::other("too many triangles for a binary STL file"))?;
100 out.write_all(&count.to_le_bytes())?;
101 for t in triangles {
102 for v in std::iter::once(&t.normal).chain(&t.corners) {
103 for x in v {
104 out.write_all(&x.to_le_bytes())?;
105 }
106 }
107 out.write_all(&0u16.to_le_bytes())?;
109 }
110 }
111 StlFormat::Ascii => {
112 let name: String = name
114 .chars()
115 .map(|c| if c.is_whitespace() { '_' } else { c })
116 .collect();
117 writeln!(out, "solid {name}")?;
118 for t in triangles {
119 let [nx, ny, nz] = t.normal;
120 writeln!(out, " facet normal {nx:e} {ny:e} {nz:e}")?;
121 writeln!(out, " outer loop")?;
122 for [x, y, z] in t.corners {
123 writeln!(out, " vertex {x:e} {y:e} {z:e}")?;
124 }
125 writeln!(out, " endloop")?;
126 writeln!(out, " endfacet")?;
127 }
128 writeln!(out, "endsolid {name}")?;
129 }
130 }
131 Ok(())
132}
133
134#[cfg(test)]
135mod tests {
136 use geop_core_math::{scalars::scal_in_f64::ScalInF64, vector::Vector3};
137 use geop_core_part::Part;
138 use geop_ops_extrude_revolve::{cube_solid, sphere::sphere_solid};
139
140 use super::*;
141 use crate::rasterize_model_tagged;
142
143 type S = ScalInF64;
144
145 fn triangles_of(part: &Part<S>) -> Vec<StlTriangle> {
146 let raster = rasterize_model_tagged(part.topology(), 8).unwrap();
147 let mut faces: Vec<FaceId> = raster.faces.keys().copied().collect();
148 faces.sort_by_key(|f| f.0);
149 stl_triangles(&raster, &faces)
150 }
151
152 fn unit_cube() -> Vec<StlTriangle> {
153 let mut part = Part::<S>::new();
154 cube_solid(
155 &mut part,
156 "c",
157 Vector3::from_array([S::ZERO; 3]),
158 Vector3::from_array([S::ONE; 3]),
159 )
160 .unwrap();
161 triangles_of(&part)
162 }
163
164 fn assert_outward(triangles: &[StlTriangle], centre: [f32; 3]) {
167 assert!(!triangles.is_empty());
168 let sub = |p: [f32; 3], q: [f32; 3]| [p[0] - q[0], p[1] - q[1], p[2] - q[2]];
169 let dot = |p: [f32; 3], q: [f32; 3]| p[0] * q[0] + p[1] * q[1] + p[2] * q[2];
170 for t in triangles {
171 let [a, b, c] = t.corners;
172 let (u, v) = (sub(b, a), sub(c, a));
173 let cross = [
174 u[1] * v[2] - u[2] * v[1],
175 u[2] * v[0] - u[0] * v[2],
176 u[0] * v[1] - u[1] * v[0],
177 ];
178 let out = sub(a, centre);
179 assert!(dot(cross, out) > 0.0, "{t:?} winds inward");
180 assert!(dot(t.normal, out) > 0.0, "{t:?} has an inward normal");
181 }
182 }
183
184 #[test]
185 fn cube_facets_face_outward() {
186 assert_outward(&unit_cube(), [0.5; 3]);
187 }
188
189 #[test]
191 fn sphere_facets_face_outward() {
192 let mut part = Part::<S>::new();
193 sphere_solid(&mut part, "s", Vector3::from_array([S::ZERO; 3]), S::ONE).unwrap();
194 assert_outward(&triangles_of(&part), [0.0; 3]);
195 }
196
197 #[test]
198 fn binary_layout() {
199 let triangles = unit_cube();
200 let mut bytes = Vec::new();
201 write_stl(&triangles, "cube", StlFormat::Binary, &mut bytes).unwrap();
202 assert_eq!(bytes.len(), 84 + 50 * triangles.len());
203 assert!(!bytes.starts_with(b"solid"));
204 assert_eq!(
205 u32::from_le_bytes(bytes[80..84].try_into().unwrap()) as usize,
206 triangles.len()
207 );
208 }
209
210 #[test]
211 fn ascii_layout() {
212 let triangles = unit_cube();
213 let mut bytes = Vec::new();
214 write_stl(&triangles, "my cube", StlFormat::Ascii, &mut bytes).unwrap();
215 let text = String::from_utf8(bytes).unwrap();
216 assert!(text.starts_with("solid my_cube\n"));
217 assert!(text.trim_end().ends_with("endsolid my_cube"));
218 assert_eq!(text.matches("facet normal").count(), triangles.len());
219 assert_eq!(text.matches("vertex").count(), 3 * triangles.len());
220 }
221}