Skip to main content

rscad_openscad/
export.rs

1use crate::*;
2pub trait ExportObject {
3    /// Exports the object to a file in OpenSCAD format.
4    fn export_as_scad(&self, output: impl AsRef<std::path::Path>) -> Result<()>;
5
6    /// Exports the object to a file in STL format.
7    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    // Write the OpenSCAD source to a uniquely-named temporary file rather than
36    // `output.with_extension("scad")`. Otherwise a user's own hand-written
37    // `model.scad` sitting next to `model.stl` would be truncated (`File::create`)
38    // and then deleted on cleanup — silent data loss even on a successful export.
39    // The `.scad` suffix is kept so the openscad CLI still recognises the input
40    // format. `NamedTempFile` removes the file when it drops, i.e. on every return
41    // path below (success or error), and only ever deletes the file we created.
42    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    /// Regression test for #75: exporting `model.stl` into a directory that
94    /// already contains a user's hand-written `model.scad` must leave that file
95    /// completely untouched. This does not depend on the `openscad` binary being
96    /// installed — whether the export succeeds or fails, the user's file must
97    /// survive, so it is safe to run in CI where `openscad` is absent.
98    #[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        // Ignore the result: `openscad` may be missing in CI (Err) or present
108        // (Ok). Either way the user's neighbouring `.scad` must be intact.
109        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}