app
Adom Chip Fetcher
Public Made by Adomby adom
Your whole parts library — manufacturer-grade chip CAD (symbol, footprint, 3D) one tap from your EDA tool.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
// Thumbnail generation by shelling to the canonical `chip-thumbnailer` CLI.
//
// chip-thumbnailer is the single source of truth for rendering the three
// per-chip artifacts under library/<MPN>/ — keeping the rendering logic
// (KiCad-CLI invocations, SVG dark theming, OCCT iso-pose render) in one
// place across every Adom tool that needs them.
//
// Output paths (canonical):
// <MPN>-symbol.svg
// <MPN>-footprint.svg
// <MPN>-3d-iso-sm.png (160×120)
// <MPN>-3d-iso.png (320×240) — primary, used by the dashboard
// <MPN>-3d-iso-lg.png (800×600) — used for full-screen pup tab on click
//
// chip-thumbnailer is idempotent: it skips artifacts whose source hasn't
// changed since the last render.
use anyhow::{Context, Result};
use std::process::Command;
use crate::library;
const CHIP_THUMBNAILER_BIN: &str = "adom-chip-thumbnailer";
const PENDING_MARKER: &str = ".thumb-pending";
pub struct GenReport {
pub mpn: String,
pub generated: Vec<&'static str>,
pub skipped: Vec<&'static str>,
pub errors: Vec<(String, String)>,
}
/// Render thumbnails for a single chip. `force = true` touches the
/// pending-marker first so chip-thumbnailer re-renders even if the output
/// is newer than the source.
pub fn generate_for(mpn: &str, force: bool) -> Result<GenReport> {
let dir = library::root().join(mpn);
if !dir.exists() {
anyhow::bail!("library/{mpn}/ does not exist");
}
if force {
let _ = std::fs::write(dir.join(PENDING_MARKER), b"");
}
let out = Command::new(CHIP_THUMBNAILER_BIN)
.arg("once")
.arg(mpn)
.output()
.context("running chip-thumbnailer once — install with `chip-thumbnailer install`")?;
let mut report = GenReport {
mpn: mpn.to_string(),
generated: Vec::new(),
skipped: Vec::new(),
errors: Vec::new(),
};
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
// Parse chip-thumbnailer's `OK: sym → ...` / `ERROR: 3d → ...` lines so
// chip-fetcher can show the same per-kind summary it always has.
for line in stdout.lines().chain(stderr.lines()) {
let l = strip_ansi(line);
if let Some(rest) = l.strip_prefix("OK: ") {
if rest.starts_with("sym ") || rest.starts_with("sym\t") || rest.starts_with("sym→") || rest.starts_with("sym ") {
report.generated.push("symbol");
} else if rest.starts_with("fp ") || rest.starts_with("fp\t") || rest.starts_with("fp→") || rest.starts_with("fp ") {
report.generated.push("footprint");
} else if rest.starts_with("3d ") || rest.starts_with("3d\t") || rest.starts_with("3d→") || rest.starts_with("3d ") {
report.generated.push("chip3d");
}
} else if let Some(rest) = l.strip_prefix("ERROR: ") {
// Best-effort kind parsing
let kind = if rest.starts_with("sym") { "symbol" }
else if rest.starts_with("fp") { "footprint" }
else if rest.starts_with("3d") { "chip3d" }
else { "render" };
report.errors.push((kind.into(), rest.trim().to_string()));
}
}
if !out.status.success() && report.errors.is_empty() {
report.errors.push(("setup".into(), format!("chip-thumbnailer exited {:?}: {}", out.status.code(), stderr.trim())));
}
// Dedup the kinds in case parsing matched twice
report.generated.sort_unstable();
report.generated.dedup();
// Cross-app: also render the ds2sf source-variant thumbnails (best-effort).
generate_ds2sf_variants(&dir, mpn);
Ok(report)
}
/// Render the ds2sf datasheet-extracted symbol/footprint to their own variant
/// thumbnails (`<mpn>-symbol.ds2sf.svg`, `<mpn>-footprint.ds2sf.svg`) so the
/// provenance carat shows a ds2sf option distinct from the vendor download.
/// Best-effort — skipped silently if the tools or the ds2sf extraction are absent.
fn generate_ds2sf_variants(dir: &std::path::Path, mpn: &str) {
// The canonical SYM/FP tile must show the ACTUAL vendor download (the source
// the carat labels "SnapEDA"/etc.), not the ds2sf datasheet synthesis.
// chip-thumbnailer prefers the ds2sf "authoritative" SVG for the footprint
// (it predates a working service-kicad path), which made the vendor tile show
// "ds2sf SYNTHESIZED". Re-render the canonical tiles from the real vendor
// files via adom-symbol/adom-footprint (service-kicad), overwriting that.
let canon_mod = dir.join(format!("{mpn}.kicad_mod"));
if canon_mod.is_file() {
let _ = Command::new("adom-footprint")
.args(["render", "--file"]).arg(&canon_mod)
.arg("--out").arg(dir.join(format!("{mpn}-footprint.svg")))
.output();
}
let sym_json = dir.join(format!("{mpn}-symbol.extracted.json"));
if sym_json.is_file() {
let out = dir.join(format!("{mpn}-symbol.ds2sf.svg"));
let _ = Command::new("adom-symbol")
.args(["render-ds2sf", "--file"])
.arg(&sym_json).arg("--out").arg(&out)
.output();
}
let fp_json = dir.join(format!("{mpn}-footprint.extracted.json"));
if fp_json.is_file() {
let out = dir.join(format!("{mpn}-footprint.ds2sf.svg"));
let _ = Command::new("adom-footprint")
.args(["render-ds2sf", "--file"])
.arg(&fp_json).arg("--out").arg(&out)
.output();
}
// The full chip-icon family (transparent iso icon, vector OUTLINE styles via
// OCCT HLR, and the name-lasered emboss + named variants) is produced by
// `chip-thumbnailer once` above — chip-thumbnailer is the chip-icon factory
// and calls service-step2glb's /outline + /name-emboss for the OCCT work.
// chip-fetcher no longer shells the local occt-*.py scripts for these.
// Canonical SYMBOL render — done LAST, AFTER chip-thumbnailer produced the
// 3D-chip OUTLINE assets (`<mpn>-3d-outline-<style>-named.svg`) in the `once`
// call. John's rule: the DEFAULT symbol shows the 3D chip as a WHITE vector
// OUTLINE with the chip name lasered on, composited into the body. We let
// `adom-symbol render` auto-discover that white outline+name asset (its
// default layout) — we no longer pass `--iso`, because the shaded raster read
// as a gray box and the BLACK "bold" outline was invisible on the dark canvas.
let canon_sym = dir.join(format!("{mpn}.kicad_sym"));
if canon_sym.is_file() {
let _ = Command::new("adom-symbol")
.args(["render", "--file"]).arg(&canon_sym)
.arg("--out").arg(dir.join(format!("{mpn}-symbol.svg")))
.output();
}
// Foreign-format libraries (EAGLE/Fusion .lbr) → convert to KiCad sym+mod via
// adom-sfconvert so each format renders its OWN geometry in the carat instead
// of falling back to the canonical KiCad render. Altium .SchLib/.PcbLib are
// OLE binary — adom-sfconvert errors honestly there, so no Altium thumb yet.
convert_foreign_libraries(dir, mpn);
// Per-source vendor variants kept by import (<mpn>.<method>.kicad_sym/.kicad_mod)
// → render each to its own thumbnail so the carat shows a preview per source.
if let Ok(rd) = std::fs::read_dir(dir) {
for e in rd.flatten() {
let name = e.file_name().to_string_lossy().to_string();
for (ext, bin, kind) in [("kicad_sym", "adom-symbol", "symbol"), ("kicad_mod", "adom-footprint", "footprint")] {
let suffix = format!(".{ext}");
let prefix = format!("{mpn}.");
if name == format!("{mpn}.{ext}") || !name.ends_with(&suffix) || !name.starts_with(&prefix) {
continue;
}
let method = &name[prefix.len()..name.len() - suffix.len()];
if method.is_empty() || method.contains('.') {
continue;
}
let out = dir.join(format!("{mpn}-{kind}.{method}.svg"));
if !out.is_file() {
let _ = Command::new(bin).args(["render", "--file"]).arg(e.path()).arg("--out").arg(&out).output();
}
}
}
}
}
/// Convert foreign-format vendor libraries into KiCad sym+mod so each format
/// renders its OWN geometry. Writes `<mpn>.<method>.kicad_sym/.kicad_mod`, which
/// the per-source variant loop above then renders to `<mpn>-<kind>.<method>.svg`.
/// Idempotent + best-effort: if the converted files already exist, or the
/// converter can't handle the format (Altium OLE binary), we leave it alone.
fn convert_foreign_libraries(dir: &std::path::Path, mpn: &str) {
// EAGLE / Fusion 360 .lbr → NATIVE via adom-sfconvert (eagle.rs, pure Rust —
// no service round-trip). Emits export .kicad_sym/.kicad_mod AND the native
// SVG thumbnails (<mpn>.fusion-symbol.svg / <mpn>.fusion-footprint.svg),
// which we move to the canonical per-source names so the variant loop skips
// the service render.
let lbr = dir.join(format!("{mpn}.lbr"));
if lbr.is_file() && !dir.join(format!("{mpn}-symbol.fusion.svg")).is_file() {
let _ = Command::new("adom-sfconvert")
.arg("convert").arg(&lbr)
.arg("--out-dir").arg(dir)
.arg("--name").arg(format!("{mpn}.fusion"))
.output();
let _ = std::fs::rename(
dir.join(format!("{mpn}.fusion-symbol.svg")),
dir.join(format!("{mpn}-symbol.fusion.svg")),
);
let _ = std::fs::rename(
dir.join(format!("{mpn}.fusion-footprint.svg")),
dir.join(format!("{mpn}-footprint.fusion.svg")),
);
}
// Altium .SchLib (symbol) + .PcbLib (footprint) → NATIVE via adom-sfconvert
// (altium-schlib / altium-pcblib, pure Rust — no service round-trip). It
// emits the export .kicad_sym/.kicad_mod AND the native SVG thumbnail
// (<mpn>.altium-symbol.svg / <mpn>.altium-footprint.svg). We move those SVGs
// to chip-fetcher's canonical per-source thumbnail names so the variant
// render loop below finds them already present and skips the service render.
let schlib = dir.join(format!("{mpn}.SchLib"));
if schlib.is_file() && !dir.join(format!("{mpn}-symbol.altium.svg")).is_file() {
let _ = Command::new("adom-sfconvert")
.arg("convert").arg(&schlib)
.arg("--out-dir").arg(dir)
.arg("--name").arg(format!("{mpn}.altium"))
.output();
let _ = std::fs::rename(
dir.join(format!("{mpn}.altium-symbol.svg")),
dir.join(format!("{mpn}-symbol.altium.svg")),
);
}
let pcblib = dir.join(format!("{mpn}.PcbLib"));
if pcblib.is_file() && !dir.join(format!("{mpn}-footprint.altium.svg")).is_file() {
let _ = Command::new("adom-sfconvert")
.arg("convert").arg(&pcblib)
.arg("--out-dir").arg(dir)
.arg("--name").arg(format!("{mpn}.altium"))
.output();
let _ = std::fs::rename(
dir.join(format!("{mpn}.altium-footprint.svg")),
dir.join(format!("{mpn}-footprint.altium.svg")),
);
}
}
// The name-laser emboss + vector-outline generation moved to chip-thumbnailer
// (the chip-icon factory), which calls service-step2glb's /name-emboss +
// /outline. chip-fetcher no longer shells the local occt-*.py scripts; the full
// chip-icon family lands via the `chip-thumbnailer once` call in generate_for.
pub fn generate_all(force: bool) -> Vec<GenReport> {
let root = library::root();
let mut reports = Vec::new();
if let Ok(entries) = std::fs::read_dir(&root) {
for entry in entries.flatten() {
if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { continue; }
let mpn = match entry.file_name().to_str() {
Some(s) => s.to_string(),
None => continue,
};
if mpn.starts_with('.') { continue; }
match generate_for(&mpn, force) {
Ok(r) => reports.push(r),
Err(e) => reports.push(GenReport {
mpn,
generated: vec![],
skipped: vec![],
errors: vec![("setup".into(), e.to_string())],
}),
}
}
}
reports
}
fn strip_ansi(s: &str) -> String {
// Drop CSI escape sequences (chip-thumbnailer color-codes its output).
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == 0x1b && i + 1 < bytes.len() && bytes[i+1] == b'[' {
i += 2;
while i < bytes.len() {
let b = bytes[i];
i += 1;
if (0x40..=0x7e).contains(&b) { break; }
}
} else {
out.push(bytes[i] as char);
i += 1;
}
}
out
}