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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
//! 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-chipsmith 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)
}
/// POST original STEP bytes + chipsmith bake-meta JSON to service-step2glb
/// /bake. Returns the baked STEP file bytes on success, or None when the
/// service hasn't deployed /bake yet (for v0.5.0 and earlier — chipsmith
/// then falls back to writing only the JSON sidecar).
pub fn bake_step(step: &[u8], bake_meta: &serde_json::Value) -> Result<Option<Vec<u8>>> {
use serde_json::json;
let url = format!("{}/bake", step2glb_service_url());
let body = json!({
"step_b64": base64_encode(step),
"bake_meta": bake_meta,
});
let body_bytes = serde_json::to_vec(&body).map_err(|e| anyhow!("encode: {e}"))?;
let resp = match ureq::post(&url)
.set("Content-Type", "application/json")
.timeout(Duration::from_secs(180))
.send_bytes(&body_bytes) {
Ok(r) => r,
Err(ureq::Error::Status(404, _)) => return Ok(None),
Err(e) => return Err(anyhow!("POST {url}: {e}")),
};
if resp.status() == 404 { return Ok(None); }
if resp.status() != 200 {
let s = resp.into_string().unwrap_or_default();
return Err(anyhow!("/bake non-200: {s}"));
}
let mut out = Vec::new();
use std::io::Read;
resp.into_reader().take(100 * 1024 * 1024).read_to_end(&mut out)
.map_err(|e| anyhow!("read body: {e}"))?;
Ok(Some(out))
}
fn base64_encode(b: &[u8]) -> String {
const TBL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity((b.len() + 2) / 3 * 4);
let mut i = 0;
while i + 3 <= b.len() {
let n = ((b[i] as u32) << 16) | ((b[i+1] as u32) << 8) | (b[i+2] as u32);
out.push(TBL[((n >> 18) & 0x3f) as usize] as char);
out.push(TBL[((n >> 12) & 0x3f) as usize] as char);
out.push(TBL[((n >> 6) & 0x3f) as usize] as char);
out.push(TBL[(n & 0x3f) as usize] as char);
i += 3;
}
let rem = b.len() - i;
if rem == 1 {
let n = (b[i] as u32) << 16;
out.push(TBL[((n >> 18) & 0x3f) as usize] as char);
out.push(TBL[((n >> 12) & 0x3f) as usize] as char);
out.push_str("==");
} else if rem == 2 {
let n = ((b[i] as u32) << 16) | ((b[i+1] as u32) << 8);
out.push(TBL[((n >> 18) & 0x3f) as usize] as char);
out.push(TBL[((n >> 12) & 0x3f) as usize] as char);
out.push(TBL[((n >> 6) & 0x3f) as usize] as char);
out.push('=');
}
out
}
/// POST raw STEP bytes to service-step2glb /step-meta. Returns the parsed
/// JSON tree (PRODUCTs + faces + surface types) on success, or None when
/// the service is on an older version that doesn't have /step-meta yet.
/// Cached at /tmp/adom-chipsmith-cache/<sha>.step-meta.json next to the GLB
/// so re-loads of the same chip skip the network call.
pub fn fetch_step_meta(step: &[u8]) -> Result<Option<serde_json::Value>> {
let _ = cache::ensure_dir()?;
let hash = cache::sha256_hex(step);
let cache_path = cache::cache_dir().join(format!("{hash}.step-meta.json"));
if cache_path.exists() {
if let Ok(s) = std::fs::read_to_string(&cache_path) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) {
return Ok(Some(v));
}
}
}
let url = format!("{}/step-meta", step2glb_service_url());
let resp = match ureq::post(&url)
.set("Content-Type", "application/step")
.timeout(Duration::from_secs(180))
.send_bytes(step) {
Ok(r) => r,
Err(ureq::Error::Status(404, _)) => return Ok(None), // service hasn't redeployed yet
Err(e) => return Err(anyhow!("POST {url}: {e}")),
};
if resp.status() == 404 {
return Ok(None);
}
if resp.status() != 200 {
let body = resp.into_string().unwrap_or_default();
return Err(anyhow!("service-step2glb /step-meta non-200: {body}"));
}
let body = resp.into_string().map_err(|e| anyhow!("read body: {e}"))?;
let _ = std::fs::write(&cache_path, &body);
let v: serde_json::Value = serde_json::from_str(&body).map_err(|e| anyhow!("parse: {e}"))?;
Ok(Some(v))
}
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
}