app
Adom Chipsmith
Public Made by Adomby adom
STEP-native chip validator — 5-source cross-validation, OCCT-generated copyright-free 3D models with embedded signal annotations, datasheet-to-STEP pipeline.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
//! Thin client to service-step2glb. Converts STEP -> GLB, caches by SHA-256.
//!
//! Mirrors the HTTP shape of step2glb's client.rs (Material structure,
//! X-Materials-Json header, X-Meshes / X-Nodes / X-Duration-Ms headers)
//! but inlines it here so adom-step has zero dependency on the step2glb crate.
use crate::cli::step2glb_service_url;
use crate::server::cache;
use anyhow::{anyhow, Result};
use std::path::PathBuf;
use std::time::Duration;
pub struct ConvertResult {
pub glb_path: PathBuf,
pub size_bytes: u64,
pub meshes: u32,
pub nodes: u32,
pub materials: serde_json::Value,
pub duration_ms: u64,
pub cache_hit: bool,
}
/// Convert raw STEP bytes via the service. Cache key = SHA-256 of bytes.
/// On hit, returns instantly without hitting the network.
pub fn convert_step_bytes(step: &[u8]) -> Result<ConvertResult> {
let dir = cache::ensure_dir()?;
let hash = cache::sha256_hex(step);
let glb = cache::glb_path(&hash);
let mat_path = cache::materials_path(&hash);
// Cache hit
if glb.exists() {
let size = std::fs::metadata(&glb).map(|m| m.len()).unwrap_or(0);
let materials = cache::read_materials(&mat_path);
return Ok(ConvertResult {
glb_path: glb,
size_bytes: size,
meshes: 0, // not re-introspected on cache hit
nodes: 0,
materials,
duration_ms: 0,
cache_hit: true,
});
}
// Cache miss — POST to service-step2glb /convert.
let url = format!("{}/convert", step2glb_service_url());
let resp = ureq::post(&url)
.set("Content-Type", "application/step")
.timeout(Duration::from_secs(180))
.send_bytes(step)
.map_err(|e| anyhow!("POST {url}: {e}"))?;
if resp.status() != 200 {
let body = resp.into_string().unwrap_or_default();
return Err(anyhow!("service-step2glb returned HTTP non-200: {body}"));
}
let meshes = header_u32(&resp, "X-Meshes");
let nodes = header_u32(&resp, "X-Nodes");
let duration_ms = u64::from(header_u32(&resp, "X-Duration-Ms"));
let mats_json_str = resp.header("X-Materials-Json").unwrap_or("[]").to_string();
let materials: serde_json::Value =
serde_json::from_str(&mats_json_str).unwrap_or(serde_json::json!([]));
// Stream body to disk in cache.
let mut tmp = dir.join(format!("{hash}.glb.tmp"));
let mut file = std::fs::File::create(&tmp)
.map_err(|e| anyhow!("create {}: {e}", tmp.display()))?;
let mut reader = resp.into_reader();
let size_bytes = std::io::copy(&mut reader, &mut file)
.map_err(|e| anyhow!("write GLB: {e}"))?;
drop(file);
std::fs::rename(&tmp, &glb).map_err(|e| anyhow!("rename {}: {e}", tmp.display()))?;
tmp.clear();
// Write materials sidecar.
let _ = std::fs::write(&mat_path, mats_json_str);
Ok(ConvertResult {
glb_path: glb,
size_bytes,
meshes,
nodes,
materials,
duration_ms,
cache_hit: false,
})
}
/// Convert from a service-kicad library: GET /convert?library=L&name=N.
pub fn convert_from_library(lib_slash_name: &str) -> Result<ConvertResult> {
let (lib, name) = lib_slash_name
.rsplit_once('/')
.ok_or_else(|| anyhow!("bad path {lib_slash_name:?}; expected <Library>/<Name>"))?;
let url = format!(
"{}/convert?library={}&name={}",
step2glb_service_url(),
urlencode(lib),
urlencode(name),
);
// Cache key for library entries: hash of the URL path itself.
let dir = cache::ensure_dir()?;
let hash = cache::sha256_hex(format!("lib:{lib}/{name}").as_bytes());
let glb = cache::glb_path(&hash);
let mat_path = cache::materials_path(&hash);
if glb.exists() {
let size = std::fs::metadata(&glb).map(|m| m.len()).unwrap_or(0);
let materials = cache::read_materials(&mat_path);
return Ok(ConvertResult {
glb_path: glb,
size_bytes: size,
meshes: 0,
nodes: 0,
materials,
duration_ms: 0,
cache_hit: true,
});
}
let resp = ureq::get(&url)
.timeout(Duration::from_secs(180))
.call()
.map_err(|e| anyhow!("GET {url}: {e}"))?;
if resp.status() != 200 {
let body = resp.into_string().unwrap_or_default();
return Err(anyhow!(
"service-step2glb returned HTTP non-200 for library {lib}/{name}: {body}"
));
}
let meshes = header_u32(&resp, "X-Meshes");
let nodes = header_u32(&resp, "X-Nodes");
let duration_ms = u64::from(header_u32(&resp, "X-Duration-Ms"));
let mats_json_str = resp.header("X-Materials-Json").unwrap_or("[]").to_string();
let materials: serde_json::Value =
serde_json::from_str(&mats_json_str).unwrap_or(serde_json::json!([]));
let tmp = dir.join(format!("{hash}.glb.tmp"));
let mut file = std::fs::File::create(&tmp)
.map_err(|e| anyhow!("create {}: {e}", tmp.display()))?;
let mut reader = resp.into_reader();
let size_bytes = std::io::copy(&mut reader, &mut file)
.map_err(|e| anyhow!("write GLB: {e}"))?;
drop(file);
std::fs::rename(&tmp, &glb).map_err(|e| anyhow!("rename: {e}"))?;
let _ = std::fs::write(&mat_path, mats_json_str);
Ok(ConvertResult {
glb_path: glb,
size_bytes,
meshes,
nodes,
materials,
duration_ms,
cache_hit: false,
})
}
fn header_u32(r: &ureq::Response, name: &str) -> u32 {
r.header(name).and_then(|s| s.parse().ok()).unwrap_or(0)
}
fn urlencode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
if matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~') {
out.push(b as char);
} else {
out.push_str(&format!("%{:02X}", b));
}
}
out
}