mod atomic;
mod error;
mod gate;
mod measure;
mod parts;
pub mod place;
mod probe;
mod probe_holes;
mod probe_warnings;
mod rules;
mod rules_hole;
mod rules_validate;
mod shrink;
#[cfg(test)]
mod atomic_tests;
#[cfg(test)]
mod place_tests;
#[cfg(test)]
mod probe_hole_tests;
#[cfg(test)]
pub(crate) mod probe_tests;
#[cfg(test)]
mod rules_boss_tests;
#[cfg(test)]
mod rules_hole_tests;
#[cfg(test)]
mod rules_support;
#[cfg(test)]
mod rules_tests;
#[cfg(test)]
mod rules_validate_tests;
use std::path::{Path, PathBuf};
use serde::Serialize;
use crate::commands::body::{generator_dir, run_script};
use crate::commands::check::CheckError;
use crate::config::FusibleConfig;
use crate::linter::FuseBlown;
pub use error::AssemblyError;
use gate::{fuse_gate, load_circuits};
use measure::{clearance, remove_stale, report_clearance};
use parts::{board_frames, board_parts, body_parts, Context, Part};
use rules::Violation;
const FORMATS: &[&str] = &["glb", "step"];
#[derive(Debug, Serialize)]
struct Output {
format: &'static str,
path: PathBuf,
}
#[derive(Debug, Serialize)]
struct Job {
mode: &'static str,
parts: Vec<Part>,
outputs: Vec<Output>,
}
pub fn run(
format: Option<String>,
output: Option<PathBuf>,
skip_lint: bool,
) -> Result<(), AssemblyError> {
let (manifest, cfg) = FusibleConfig::nearest().ok_or(AssemblyError::Missing)?;
let assembly = cfg.assembly.clone().ok_or(AssemblyError::Missing)?;
let root = manifest.parent().unwrap_or(Path::new("."));
let formats = formats(format.or(assembly.format.clone()))?;
let out_dir = out_dir(output.or(assembly.out_dir.clone()), root)?;
let parts_dir = out_dir.join("assembly");
remove_stale(&parts_dir, &out_dir);
let circuits = fuse_gate(&cfg, &assembly, skip_lint)?;
let frames = board_frames(&cfg, &assembly);
let ctx = Context {
cfg: &cfg,
root,
parts_dir: &parts_dir,
circuits: &circuits,
frames: &frames,
};
let bodies = body_parts(&ctx, &assembly)?;
if !skip_lint {
report_clearance(&ctx, &bodies, &parts_dir)?;
}
let mut parts = board_parts(&ctx, &assembly)?;
parts.extend(bodies);
let outputs = outputs(&formats, &out_dir);
merge(Job {
mode: "merge",
parts,
outputs,
})
}
fn out_dir(chosen: Option<PathBuf>, root: &Path) -> std::io::Result<PathBuf> {
std::path::absolute(chosen.unwrap_or_else(|| root.join("dist")))
}
fn outputs(formats: &[&'static str], out_dir: &Path) -> Vec<Output> {
formats
.iter()
.map(|&format| Output {
format,
path: out_dir.join(format!("assembly.{format}")),
})
.collect()
}
pub fn verify(cfg: &FusibleConfig, root: &Path) -> Result<(), AssemblyError> {
let Some(assembly) = &cfg.assembly else {
return Ok(());
};
let circuits = load_circuits(cfg, assembly)?;
let scratch = generator_dir().join(format!("check-{}", std::process::id()));
let frames = board_frames(cfg, assembly);
let ctx = Context {
cfg,
root,
parts_dir: &scratch,
circuits: &circuits,
frames: &frames,
};
let found = body_parts(&ctx, assembly).and_then(|bodies| clearance(&ctx, &bodies, None));
let _ = std::fs::remove_dir_all(&scratch);
blow_if_any(&found?)
}
fn blow_if_any(found: &[Violation]) -> Result<(), AssemblyError> {
if found.is_empty() {
return Ok(());
}
let diagnostics = found.iter().map(Violation::diagnostic).collect();
Err(CheckError::FuseBlown(FuseBlown::new(diagnostics)).into())
}
fn formats(format: Option<String>) -> Result<Vec<&'static str>, AssemblyError> {
match format.as_deref().unwrap_or("glb") {
"all" => Ok(FORMATS.to_vec()),
name => FORMATS
.iter()
.find(|f| **f == name)
.map(|f| vec![*f])
.ok_or_else(|| AssemblyError::UnknownFormat {
format: name.to_string(),
}),
}
}
fn merge(job: Job) -> Result<(), AssemblyError> {
println!("🧩 Merging {} parts with OpenCascade...", job.parts.len());
let payload = serde_json::to_vec(&job).map_err(std::io::Error::other)?;
run_script("assembly.ts", &payload)?;
for output in &job.outputs {
println!(
"✅ Assembly ({}) -> {}",
output.format,
output.path.display()
);
}
Ok(())
}