mod convert;
mod edges;
mod eject;
mod eject_edit;
mod eject_files;
mod eject_manifest;
mod error;
mod generator;
mod generator_setup;
mod jobs;
mod kit;
mod kit_data;
mod kit_levels;
mod layout;
mod settings;
mod spec;
mod step;
mod yaml_lines;
#[cfg(test)]
mod eject_comment_tests;
#[cfg(test)]
mod eject_files_tests;
#[cfg(test)]
mod eject_tests;
#[cfg(test)]
mod generator_tests;
#[cfg(test)]
mod jobs_tests;
#[cfg(test)]
mod kit_tests;
#[cfg(test)]
mod layout_tests;
#[cfg(test)]
mod tests;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::commands::build::execute_shell;
use crate::commands::check;
use crate::commands::target::BoardTarget;
use crate::config::{BodyConfig, EnclosureConfig, FusibleConfig};
use crate::gerber::PcbModel;
pub(crate) use convert::convert_to_glb;
pub use eject::eject;
pub use error::BodyError;
use generator::run_generator;
pub(crate) use generator::{generator_dir, run_script};
use jobs::{select_jobs, Job};
pub use kit::kit;
pub(crate) use kit_data::{build_kit, Kit, KitOpening};
use settings::{preview_settings, resolve_settings, Settings};
use spec::{build_spec, preview_from, Output, Spec};
pub(crate) use step::generate_step;
const FORMATS: &[&str] = &["step", "stl", "glb", "brep"];
const DEFAULT_WALL: f64 = 2.0;
const DEFAULT_BOARD_THICKNESS: f64 = 1.6;
const CLEARANCE: f64 = 0.5;
const STANDOFF: f64 = 3.0;
const HEADROOM: f64 = 12.0;
const BOSS_WALL: f64 = 2.0;
const PILOT_UNDERSIZE: f64 = 0.7;
const EDGE_TOLERANCE: f64 = 0.5;
const NOTCH_MARGIN: f64 = 0.5;
const VENT_INSET: f64 = 4.0;
#[derive(Debug, Clone, PartialEq)]
pub struct BodyPreview {
pub glb: Vec<u8>,
pub board_z: f64,
pub board_thickness: f64,
pub height: f64,
pub wall: f64,
pub size: [f64; 3],
}
static PREVIEW_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn run(
target: Option<PathBuf>,
overrides: EnclosureConfig,
skip_lint: bool,
) -> Result<(), BodyError> {
let (manifest, cfg) = FusibleConfig::nearest().unzip();
let root = manifest
.as_deref()
.and_then(Path::parent)
.unwrap_or(Path::new("."));
for job in select_jobs(&cfg.unwrap_or_default(), target)? {
match job {
Job::Authored { name, body } => build_authored(&name, &body, root)?,
Job::Generated { name, body, board } => {
let settings = resolve_settings(overrides.clone(), body.enclosure)?;
build_generated(&name, &board, &settings, skip_lint)?;
}
}
}
Ok(())
}
pub(crate) fn build_authored(name: &str, body: &BodyConfig, root: &Path) -> Result<(), BodyError> {
let path = body.source.clone().unwrap_or_default();
if let Some(command) = &body.build {
println!("🛠️ Building body `{name}`: \"{command}\"...");
let failed = || BodyError::BuildFailed {
name: name.to_string(),
command: command.clone(),
};
let status = execute_shell(command, Some(root)).map_err(|_| failed())?;
status.success().then_some(()).ok_or_else(failed)?;
}
if !path.is_file() {
return Err(BodyError::MissingSource {
name: name.to_string(),
path,
});
}
println!("✅ Body {name} (authored) -> {}", path.display());
Ok(())
}
fn build_generated(
name: &str,
board: &BoardTarget,
settings: &Settings,
skip_lint: bool,
) -> Result<(), BodyError> {
let circuit_json = fuse_gate(board, skip_lint)?;
let out_dir = std::path::absolute(&settings.out_dir)?;
let outputs = settings
.formats
.iter()
.map(|&format| Output {
format,
path: out_dir.join(format!("{name}.{format}")),
})
.collect();
std::fs::create_dir_all(&out_dir)?;
for output in generate(&circuit_json, settings, outputs)?.outputs {
println!(
"✅ Body {name} ({}) -> {}",
output.format,
output.path.display()
);
}
Ok(())
}
pub fn preview(
circuit_json: &str,
manifest: Option<EnclosureConfig>,
) -> Result<BodyPreview, BodyError> {
let settings = preview_settings(manifest.unwrap_or_default())?;
let id = PREVIEW_COUNTER.fetch_add(1, Ordering::Relaxed);
let path = generator_dir().join(format!("preview-{}-{id}.glb", std::process::id()));
let outputs = vec![Output {
format: "glb",
path: path.clone(),
}];
let result = generate(circuit_json, &settings, outputs)
.and_then(|spec| Ok(preview_from(&spec, std::fs::read(&path)?)));
let _ = std::fs::remove_file(&path);
result
}
pub(crate) fn preview_config(board: Option<&str>) -> Option<EnclosureConfig> {
FusibleConfig::nearest().and_then(|(_, cfg)| fitted_settings(cfg, board))
}
fn fitted_settings(cfg: FusibleConfig, board: Option<&str>) -> Option<EnclosureConfig> {
cfg.bodies
.into_values()
.find(|body| {
body.generate.is_some() && (body.board.is_none() || body.board.as_deref() == board)
})
.map(|body| body.enclosure)
}
fn fuse_gate(board: &BoardTarget, skip_lint: bool) -> Result<String, BodyError> {
if skip_lint {
println!("⚠️ Skipping electrical rule verification (--skip-lint requested).\n");
return Ok(check::load_circuit_source(&board.entry)?.1);
}
println!("🔍 Verifying electrical rules before generating bodies (fuse check)...");
let (_, source) = check::lint_source(&board.entry, board.mcu.clone())?;
println!("⚡ Fuse intact — proceeding with body generation.\n");
Ok(source)
}
fn generate(
circuit_json: &str,
settings: &Settings,
outputs: Vec<Output>,
) -> Result<Spec, BodyError> {
let spec = build_spec(&PcbModel::parse(circuit_json), settings, outputs)?;
run_generator(&spec)?;
Ok(spec)
}