univert 0.4.0

Universal file converter (library and CLI)
Documentation
use std::process::Command;

use crate::{Error, Job, Result};

pub fn to_csg(job: &Job) -> Result<()> {
  openscad(job, "csg")
}

pub fn to_off(job: &Job) -> Result<()> {
  openscad(job, "off")
}

pub fn to_png(job: &Job) -> Result<()> {
  openscad(job, "png")
}

pub fn to_stl(job: &Job) -> Result<()> {
  openscad(job, "stl")
}

fn openscad(job: &Job, _out_ext: &str) -> Result<()> {
  require_tool("openscad")?;

  let status = Command::new("openscad")
    .arg("-o")
    .arg(job.output)
    .arg(job.input)
    .status()
    .map_err(|e| Error::ToolFailed {
      tool: "openscad",
      message: e.to_string(),
    })?;

  if !status.success() {
    return Err(Error::ToolFailed {
      tool: "openscad",
      message: format!("openscad exited with status {status}"),
    });
  }
  Ok(())
}

pub fn stl_to_png(job: &Job) -> Result<()> {
  require_tool("openscad")?;

  // Create a temp SCAD file that imports the STL, then render it.
  let wrapper = tempfile::Builder::new()
    .prefix("univert-stl-")
    .suffix(".scad")
    .tempfile()?;
  std::fs::write(
    wrapper.path(),
    format!("import(\"{}\");\n", job.input.display()),
  )?;

  let status = Command::new("openscad")
    .arg("-o")
    .arg(job.output)
    .arg(wrapper.path())
    .status()
    .map_err(|e| Error::ToolFailed {
      tool: "openscad",
      message: e.to_string(),
    })?;

  if !status.success() {
    return Err(Error::ToolFailed {
      tool: "openscad",
      message: format!("openscad exited with status {status}"),
    });
  }
  Ok(())
}

fn require_tool(name: &'static str) -> Result<()> {
  if which_tool(name) {
    Ok(())
  } else {
    Err(Error::MissingTool {
      from: String::new(),
      to: String::new(),
      tool: name,
    })
  }
}

pub(crate) fn which_tool(name: &str) -> bool {
  if let Ok(path) = std::env::var("PATH") {
    for dir in std::env::split_paths(&path) {
      let candidate = dir.join(name);
      if candidate.is_file() {
        return true;
      }
      // Windows-style
      let candidate = dir.join(format!("{name}.exe"));
      if candidate.is_file() {
        return true;
      }
    }
  }
  false
}