app
Adom Chip Thumbnailer
Public Made by Adomby adom
The chip-icon factory: from a STEP file it renders the complete family of chip icons, shaded 3D orientations, transparent icons, name-lasered variants, and 11 vector outline styles, in a live web app
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
use crate::library;
use crate::queue;
use crate::ui::{err, hint, ok, warn, CYAN, DIM, RESET};
use crate::{install, manifest, note, render_3d};
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
use std::process::Command as ProcCommand;
#[derive(Parser, Debug)]
#[command(
name = "adom-chip-thumbnailer",
version,
about = "Render symbol/footprint/3D thumbnails for chip-fetcher library entries.",
long_about = "\
chip-thumbnailer renders three thumbnails per chip in the chip-fetcher\n\
library and drops them next to the source files:\n \n\
<MPN>-symbol.svg Adom-themed KiCad symbol (kicad-cli + svg-theme)\n\
<MPN>-footprint.svg FpView-styled footprint (kicad-cli + dark grid)\n\
<MPN>-3d-iso.png OCCT-direct render via service-step2glb /thumbnail\n\
"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand, Debug)]
pub enum Command {
/// Render all three thumbnails for one chip (writes alongside the source files).
Once(OnceArgs),
/// Render every chip in the library that has a `.thumb-pending` marker.
Scan,
/// Show which thumbnails exist + which sources are present for one chip.
Status(StatusArgs),
/// Re-render only one kind for one chip (sym, fp, 3d, or all).
Rerender(RerenderArgs),
/// Probe local kicad-cli + step2glb + service-step2glb reachability.
Health,
/// Print resolved library root + step2glb binary location.
Config,
/// Manually mark a chip's thumbnails as out-of-date (touches `.thumb-pending`).
TouchPending(TouchPendingArgs),
/// Install SKILL.md + bash completions (fetches fresh SKILL.md from the wiki).
Install,
/// Print shell completions to stdout: `chip-thumbnailer completions bash`
Completions(CompletionsArgs),
/// Run the web app: a live dashboard that renders every icon for a STEP
/// file in real-time, with per-icon size / format / transparency.
Serve(ServeArgs),
/// Open the web app in a Hydrogen webview tab (starts `serve` if needed).
App(ServeArgs),
}
#[derive(Args, Debug)]
pub struct ServeArgs {
/// Port to bind the web app on.
#[arg(long, env = "CHIP_THUMBNAILER_PORT", default_value = "8794")]
pub port: u16,
}
#[derive(Args, Debug)]
pub struct OnceArgs {
pub mpn: String,
}
#[derive(Args, Debug)]
pub struct StatusArgs {
pub mpn: String,
}
#[derive(Args, Debug)]
pub struct RerenderArgs {
pub mpn: String,
/// Which thumbnail to re-render. Default = all three.
#[arg(long, default_value = "all")]
pub kind: String,
}
#[derive(Args, Debug)]
pub struct TouchPendingArgs {
pub mpn: String,
}
#[derive(Args, Debug)]
pub struct CompletionsArgs {
#[arg(value_enum)]
pub shell: ShellArg,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ShellArg {
Bash,
Zsh,
Fish,
}
impl From<ShellArg> for clap_complete::Shell {
fn from(s: ShellArg) -> Self {
match s {
ShellArg::Bash => clap_complete::Shell::Bash,
ShellArg::Zsh => clap_complete::Shell::Zsh,
ShellArg::Fish => clap_complete::Shell::Fish,
}
}
}
// ────────────────────────────────────────────────────────────────────────
// command implementations
// ────────────────────────────────────────────────────────────────────────
pub fn once_cmd(args: OnceArgs) {
match queue::render_one(&args.mpn) {
Ok(report) => {
if report.all_ok() {
ok(format!(
"{CYAN}{}{RESET} — 3D orientations + transparent icons rendered, .thumb-pending cleared",
args.mpn
));
} else {
warn(format!(
"{CYAN}{}{RESET} — 3D render FAILED. See .thumb-errors.json",
args.mpn,
));
}
// Build the AI-hint payload by reading back the freshly-written
// manifest so the JSON output of `once` is self-describing —
// any AI agent driving chip-thumbnailer learns from one
// command both what was rendered AND how to use it.
let chip = library::ChipPaths::for_mpn(&args.mpn).ok();
let m = chip.as_ref().map(manifest::build);
let mut next_steps: Vec<String> = Vec::new();
if report.all_ok() {
if let Some(ref c) = chip {
next_steps.push(format!(
"Manifest written: cat library/{}/{}-thumbs.json",
c.mpn, c.mpn
));
next_steps.push(format!(
"Open the 3D in interactive mode: adom-step view library/{}/{}.step",
c.mpn, c.mpn
));
next_steps.push(format!(
"Open the chip icons as VS Code tabs: adom-vscode open library/{}/{}-3d-iso.png && adom-vscode open library/{}/{}-3d-iso-icon.png",
c.mpn, c.mpn, c.mpn, c.mpn
));
}
}
if !report.all_ok() {
next_steps.push(format!(
"Inspect failures: cat library/{}/.thumb-errors.json",
args.mpn
));
next_steps.push(format!(
"Retry the render: chip-thumbnailer rerender {} --kind 3d",
args.mpn
));
}
println!(
"{}",
serde_json::to_string(&serde_json::json!({
"ok": report.all_ok(),
"mpn": report.mpn,
"3d": report.three_d_ok,
"errors": report.errors,
"manifest": m,
"_hints": {
"purpose": "These thumbnails are sized for card / index / tooltip surfaces. For full-fidelity inspection, see manifest.thumbnails.<kind>.full_fidelity.tool / .command.",
"palette": "All thumbnails use the canonical Adom palette in manifest.palette — match your surrounding UI to canvas_bg #0d1117 and accent_teal #00b8b0 to avoid clashing.",
"sizes": "3D PNGs come in sm (160×120), md (320×240, no suffix — back-compat), lg (800×600). SVGs scale infinitely; one file each.",
"atmosphere": "Themed SVGs bake in the dark canvas + 2.54 mm PCB grid + faint teal radial glow. If your container already provides those (e.g. SymView), don't double them.",
"next_steps": next_steps,
}
})).unwrap()
);
if !report.all_ok() {
std::process::exit(3);
}
}
Err(e) => {
err(format!("once failed for {}: {e}", args.mpn));
hint("Run `chip-thumbnailer status <MPN>` to inspect the chip dir.");
std::process::exit(1);
}
}
}
pub fn scan_cmd() {
match queue::render_pending() {
Ok(0) => {
ok("no chips with .thumb-pending markers — nothing to do");
}
Ok(n) => {
ok(format!("processed {n} pending chip(s)"));
}
Err(e) => {
err(format!("scan failed: {e}"));
std::process::exit(1);
}
}
}
pub fn status_cmd(args: StatusArgs) {
let chip = match library::ChipPaths::for_mpn(&args.mpn) {
Ok(c) => c,
Err(e) => {
err(format!("{e}"));
std::process::exit(1);
}
};
let yes = "✓";
let no = "·";
note!("{CYAN}{}{RESET}", chip.mpn);
note!(" {DIM}dir{RESET} {}", chip.dir.display());
note!(
" {DIM}sources{RESET} sym={} mod={} step={}",
if chip.kicad_sym.is_some() { yes } else { no },
if chip.kicad_mod.is_some() { yes } else { no },
if chip.step.is_some() { yes } else { no },
);
note!(
" {DIM}thumbs{RESET} sym={} fp={} 3d={}",
if chip.symbol_thumb().is_file() { yes } else { no },
if chip.footprint_thumb().is_file() { yes } else { no },
if chip.three_d_thumb().is_file() { yes } else { no },
);
note!(
" {DIM}pending{RESET} {}",
if chip.thumb_pending { "yes (.thumb-pending)" } else { "no" }
);
}
pub fn rerender_cmd(args: RerenderArgs) {
let chip = match library::ChipPaths::for_mpn(&args.mpn) {
Ok(c) => c,
Err(e) => {
err(format!("{e}"));
std::process::exit(1);
}
};
// chip-thumbnailer renders chip icons only; `3d` (the 3D orientations +
// transparent icons) is the sole kind. `all` is kept as an alias.
let mut had_failure = false;
if !["all", "3d"].contains(&args.kind.as_str()) {
err(format!("unknown --kind `{}`; chip-thumbnailer renders chip icons, so the only kind is `3d` (or `all`)", args.kind));
std::process::exit(1);
}
match render_3d::render(&chip) {
Ok(_) => ok(format!("3d → {}", chip.three_d_thumb().display())),
Err(e) => { err(format!("3d failed: {e}")); had_failure = true; }
}
// Refresh the manifest so file lists, palette, version stay in sync
// with what's on disk — same contract as `once`.
match manifest::write(&chip) {
Ok(path) => ok(format!("manifest → {}", path.display())),
Err(e) => warn(format!("manifest write failed: {e}")),
}
if had_failure {
std::process::exit(3);
}
}
pub fn health_cmd() {
let kicad_ok = ProcCommand::new("kicad-cli").arg("version").output().is_ok();
let step2glb_ok = ProcCommand::new("adom-step2glb").arg("--version").output().is_ok();
let svc_ok = ureq::get("https://step2glb-gmdoncpxdwx0.adom.cloud/health")
.timeout(std::time::Duration::from_secs(5))
.call()
.is_ok();
let lib_ok = library::root().is_dir();
note!("{DIM}kicad-cli{RESET} {}", if kicad_ok { "ok" } else { "MISSING" });
note!("{DIM}step2glb{RESET} {}", if step2glb_ok { "ok" } else { "MISSING" });
note!("{DIM}service-step2glb{RESET} {}", if svc_ok { "reachable" } else { "UNREACHABLE" });
note!("{DIM}library{RESET} {} ({})",
library::root().display(),
if lib_ok { "exists" } else { "MISSING" }
);
if !(kicad_ok && step2glb_ok && svc_ok && lib_ok) {
std::process::exit(2);
}
}
pub fn config_cmd() {
note!("{DIM}library root:{RESET} {}", library::root().display());
note!("{DIM}kicad-cli:{RESET} {}", which_or("kicad-cli"));
note!("{DIM}step2glb:{RESET} {}", which_or("step2glb"));
note!("{DIM}service-step2glb:{RESET} https://step2glb-gmdoncpxdwx0.adom.cloud");
}
pub fn touch_pending_cmd(args: TouchPendingArgs) {
match library::ChipPaths::for_mpn(&args.mpn) {
Ok(c) => match library::touch_pending(&args.mpn) {
Ok(_) => ok(format!(
"marked {} as pending — next `scan` will re-render",
c.mpn
)),
Err(e) => {
err(format!("{e}"));
std::process::exit(1);
}
},
Err(e) => {
err(format!("{e}"));
std::process::exit(1);
}
}
}
pub fn install_cmd() {
install::run();
}
pub fn completions_cmd(args: CompletionsArgs) {
let shell: clap_complete::Shell = args.shell.into();
let mut cmd = Cli::command();
clap_complete::generate(shell, &mut cmd, "chip-thumbnailer", &mut std::io::stdout());
}
pub fn serve_cmd(args: ServeArgs) {
if let Err(e) = crate::serve::run(args.port) {
err(format!("serve failed: {e}"));
std::process::exit(1);
}
}
pub fn app_cmd(args: ServeArgs) {
match crate::serve::open_in_hydrogen(args.port) {
Ok(_) => {}
Err(e) => {
err(format!("could not open app: {e}"));
hint("Is the server running? Try `chip-thumbnailer serve` in another shell.");
std::process::exit(1);
}
}
}
fn which_or(name: &str) -> String {
ProcCommand::new("which")
.arg(name)
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| format!("(not on PATH)"))
}