1use crate::*;
2pub trait ExportObject {
3 fn export_as_scad(&self, output: impl AsRef<std::path::Path>) -> Result<()>;
5
6 fn export_as_stl(&self, output: impl AsRef<std::path::Path>) -> Result<()>;
8}
9
10impl<T: OpenscadCodegen> ExportObject for T {
11 fn export_as_scad(&self, output: impl AsRef<std::path::Path>) -> Result<()> {
12 let out = self.render()?;
13 let mut file = std::fs::File::create(output.as_ref())
14 .change_context(Error)
15 .attach("Failed to create openscad file")?;
16 use std::io::Write;
17 file.write_all(out.as_bytes()).cc()?;
18 Ok(())
19 }
20
21 fn export_as_stl(&self, output: impl AsRef<std::path::Path>) -> Result<()> {
22 export_as_stl(self, output)
23 }
24}
25
26fn export_as_stl<T: OpenscadCodegen>(
27 object: &T,
28 output: impl AsRef<std::path::Path>,
29) -> Result<()> {
30 use std::io::Write;
31
32 let out = object.render()?;
33 let stl_path = output.as_ref().with_extension("stl");
34
35 let mut scad_file = tempfile::Builder::new()
43 .prefix("rscad-export-")
44 .suffix(".scad")
45 .tempfile()
46 .change_context(Error)
47 .attach("Failed to create temporary openscad file")?;
48 scad_file.write_all(out.as_bytes()).cc()?;
49 scad_file.flush().cc()?;
50 let scad_path = scad_file.path();
51
52 let t0 = std::time::Instant::now();
53 tracing::info!(
54 "Invoking openscad: {} -o {}",
55 scad_path.display(),
56 stl_path.display()
57 );
58 let status = std::process::Command::new("openscad")
59 .stderr(std::process::Stdio::inherit())
60 .stdout(std::process::Stdio::inherit())
61 .arg(scad_path)
62 .arg("-o")
63 .arg(&stl_path)
64 .status();
65 let status = match status {
66 Ok(s) => s,
67 Err(e) => {
68 tracing::error!("Failed to launch `openscad` binary: {e}");
69 return Err(e)
70 .change_context(Error)
71 .attach("Failed to launch `openscad` — is it installed and on PATH?");
72 }
73 };
74 if !status.success() {
75 tracing::error!("openscad exited with {status} after {:.1?}", t0.elapsed());
76 return Err(Error)
77 .attach("Failed to export STL")
78 .attach("The OpenSCAD command did not complete successfully.");
79 }
80 tracing::info!(
81 "STL exported: {} in {:.1?}",
82 stl_path.display(),
83 t0.elapsed()
84 );
85 Ok(())
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::cube::Cube;
92
93 #[test]
99 fn export_stl_does_not_clobber_existing_scad() {
100 let dir = tempfile::tempdir().expect("BUG: failed to create temp dir");
101 let user_scad = dir.path().join("model.scad");
102 let stl_path = dir.path().join("model.stl");
103
104 let user_content = "// user's precious hand-written scad\ncube([10, 20, 30]);\n";
105 std::fs::write(&user_scad, user_content).expect("BUG: failed to seed user scad");
106
107 let _ = Cube::new([1, 1, 1]).export_as_stl(&stl_path);
110
111 let after = std::fs::read_to_string(&user_scad)
112 .expect("user's model.scad must still exist after export");
113 assert_eq!(
114 after, user_content,
115 "user's model.scad was truncated or rewritten by export_as_stl"
116 );
117 }
118}