use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use super::spec::Spec;
use super::MechanicalError;
use crate::tscircuit::detect::find_binary_in_path;
const GENERATOR_TS: &str = include_str!("../../assets/mechanical.ts");
const OCCT_WASM_VERSION: &str = "5.7.1";
pub(super) fn generator_dir() -> PathBuf {
std::env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))
.or_else(|| std::env::var_os("LOCALAPPDATA").map(PathBuf::from))
.unwrap_or_else(std::env::temp_dir)
.join("fusible")
.join(format!("mechanical-{}", env!("CARGO_PKG_VERSION")))
}
pub(super) fn run_generator(spec: &Spec) -> Result<(), MechanicalError> {
let bun = std::env::var_os("PATH")
.and_then(|p| find_binary_in_path("bun", &p))
.ok_or(MechanicalError::BunMissing)?;
let dir = generator_dir();
prepare_generator(&bun, &dir)?;
println!("🧊 Building mechanical model with OpenCascade...");
let mut child = Command::new(&bun)
.arg("generator.ts")
.current_dir(&dir)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()?;
let payload = serde_json::to_vec(spec).map_err(std::io::Error::other)?;
child
.stdin
.take()
.expect("stdin is piped")
.write_all(&payload)?;
let out = child.wait_with_output()?;
if out.status.success() {
return Ok(());
}
Err(MechanicalError::GeneratorFailed {
command: format!("bun {}", dir.join("generator.ts").display()),
code: out.status.code(),
stderr: String::from_utf8_lossy(&out.stderr).trim().to_string(),
})
}
fn package_json() -> String {
format!(
"{{\"name\":\"fusible-mechanical\",\"private\":true,\"type\":\"module\",\
\"dependencies\":{{\"occt-wasm\":\"{OCCT_WASM_VERSION}\"}}}}\n"
)
}
fn prepare_generator(bun: &Path, dir: &Path) -> Result<(), MechanicalError> {
std::fs::create_dir_all(dir)?;
std::fs::write(dir.join("generator.ts"), GENERATOR_TS)?;
std::fs::write(dir.join("package.json"), package_json())?;
if dir.join("node_modules/occt-wasm/package.json").exists() {
return Ok(());
}
println!("📦 Installing occt-wasm {OCCT_WASM_VERSION} (first run only)...");
let status = Command::new(bun)
.arg("install")
.current_dir(dir)
.stdin(Stdio::null())
.status()?;
if status.success() {
return Ok(());
}
Err(MechanicalError::GeneratorFailed {
command: "bun install".into(),
code: status.code(),
stderr: format!("could not install occt-wasm into {}", dir.display()),
})
}