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 + CLI.
master
John Lauer
1.2.0: part number placed by the mark planner (long axis, upright with pin 1 toward the viewer, same mark as the etched…
3681866
18d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
//! The part-number mark, placed by construction: the named outlines and the name-lasered icon come
//! from the shared MARK PLANNER (adom-aiflow's `tools/laser-etch`, vendored here in `planner/` and
//! installed to ~/.local/share/adom-chip-thumbnailer/planner). The same code as `adom-aiflow-etch`, so
//! the thumbnailer and the etch can never disagree:
//!
//! * the mark runs along the longest usable dimension (chip R/C/L between the end caps, ICs along the
//! pin rows, LEDs on the front side wall), as large as fits, one line or two equal lines;
//! * the outline pose is the standard iso pose: pin 1 (pins 1 to 3) toward the viewer, the name upright
//! and left to right; the outline `rot` is the planner's, never a fixed 180 / 90;
//! * every named SVG is gated; a failing one is written as `*.FAILED-GATE.svg` with a loud warning and
//! Hint lines, never silently as `-named`.
//!
//! Plain python3 only (stdlib); the geometry runs on the shared service-step2glb.
use crate::library::ChipPaths;
use anyhow::{anyhow, Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
pub struct MarkOutcome {
pub status: String,
pub plan: Option<PathBuf>,
pub posed: Option<PathBuf>,
pub etched: Option<PathBuf>,
pub outlines: Vec<(PathBuf, String)>,
pub summary: String,
pub hints: Vec<String>,
}
/// Where the planner's Python lives: $ADOM_MARK_PLANNER, this package's installed copy, the binary's
/// sibling `planner/`, or an installed adom-aiflow (`adom-aiflow-etch` sits next to the same files).
pub fn planner_dir() -> Option<PathBuf> {
let ok = |p: &Path| p.join("mark_outline.py").is_file() && p.join("mark_etch.py").is_file();
if let Ok(p) = std::env::var("ADOM_MARK_PLANNER") {
let p = PathBuf::from(p);
if ok(&p) {
return Some(p);
}
}
let home = std::env::var("HOME").unwrap_or_default();
let mut cands = vec![PathBuf::from(&home).join(".local/share/adom-chip-thumbnailer/planner")];
if let Ok(exe) = std::env::current_exe() {
if let Some(d) = exe.parent() {
cands.push(d.join("planner"));
cands.push(d.join("../planner"));
}
}
if let Ok(t) = std::fs::canonicalize(PathBuf::from(&home).join(".local/bin/adom-aiflow-etch")) {
if let Some(d) = t.parent() {
cands.push(d.to_path_buf());
}
}
cands.into_iter().find(|p| ok(p))
}
fn last_json(stdout: &str) -> Option<serde_json::Value> {
stdout.lines().rev().find(|l| l.trim_start().starts_with('{')).and_then(|l| serde_json::from_str(l).ok())
}
fn run_py(dir: &Path, script: &str, args: &[String]) -> Result<(bool, String, String)> {
let out = Command::new(std::env::var("AIFLOW_PYTHON").unwrap_or_else(|_| "python3".into()))
.arg(dir.join(script))
.args(args)
.env("AIFLOW_ETCH_CLIENT", "chip-thumbnailer/chip-fetcher")
.output()
.with_context(|| format!("run python3 {script} (is python3 installed?)"))?;
Ok((
out.status.success(),
String::from_utf8_lossy(&out.stdout).to_string(),
String::from_utf8_lossy(&out.stderr).to_string(),
))
}
fn hints_of(stderr: &str) -> Vec<String> {
stderr.lines().filter(|l| l.starts_with("Hint:") || l.starts_with("ERROR")).map(str::to_string).collect()
}
/// Etch (if needed) and draw the named outlines for one chip; writes into the chip folder:
/// <MPN>-etched.step the model with its flat Adom.LaserEtch mark
/// <MPN>-3d-outline-<style>.svg plain outlines, standard pose
/// <MPN>-3d-outline-<style>-named.svg the name = the mark (gate PASS/WARN)
/// <MPN>-3d-outline-<style>-named.FAILED-GATE.svg only when the gate failed (loud warning)
/// <MPN>.mark-plan.json the planner's JSON (schema adom/mark-plan@1)
pub fn named_family(chip: &ChipPaths, up: Option<&str>) -> Result<MarkOutcome> {
let dir = planner_dir().ok_or_else(|| {
anyhow!("the mark planner is not installed (reinstall adom-chip-thumbnailer, or install adom-aiflow; or set ADOM_MARK_PLANNER)")
})?;
let step = chip.step.as_ref().ok_or_else(|| anyhow!("no .step for {}", chip.mpn))?;
let mut hints = Vec::new();
let mut common: Vec<String> = Vec::new();
if let Some(fp) = &chip.kicad_mod {
common.push("--footprint".into());
common.push(fp.display().to_string());
} else {
hints.push("Hint: WARNING no <MPN>.kicad_mod beside the STEP: pin 1 is assumed at the negative end; add the footprint for a certain reading direction".into());
}
if let Some(u) = up {
common.push("--up".into());
common.push(u.into());
}
let etched = chip.dir.join(format!("{}-etched.step", chip.mpn));
let mut have_etched = etched.is_file();
if !have_etched {
let mut a = vec!["--in".into(), step.display().to_string(), "--mpn".into(), chip.mpn.clone(), "--out".into(), etched.display().to_string()];
a.extend(common.iter().cloned());
let (ok, _so, se) = run_py(&dir, "mark_etch.py", &a)?;
hints.extend(hints_of(&se));
have_etched = ok && etched.is_file();
if !have_etched {
hints.push("Hint: no etched STEP (see the lines above); the named outline is drawn from the plain model on the planned band".into());
}
}
let posed = chip.dir.join(format!(".posed-{}.step", chip.mpn));
let mut a = vec![
step.display().to_string(),
"--mpn".into(),
chip.mpn.clone(),
"--out-dir".into(),
chip.dir.display().to_string(),
"--styles".into(),
"all".into(),
"--stem".into(),
chip.mpn.clone(),
"--write-plain".into(),
"--posed-out".into(),
posed.display().to_string(),
];
a.extend(common.iter().cloned());
if have_etched {
a.push("--etched".into());
a.push(etched.display().to_string());
}
let (_ok, so, se) = run_py(&dir, "mark_outline.py", &a)?;
hints.extend(hints_of(&se));
let v = last_json(&so).ok_or_else(|| anyhow!("mark_outline.py gave no result: {}", se.lines().last().unwrap_or("")))?;
let outlines = v["outlines"]
.as_array()
.map(|a| {
a.iter()
.map(|o| (PathBuf::from(o["path"].as_str().unwrap_or("")), o["status"].as_str().unwrap_or("?").to_string()))
.collect()
})
.unwrap_or_default();
let plan = v["plan"].as_str().map(PathBuf::from);
let summary = plan
.as_ref()
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok())
.and_then(|j| j["summary"].as_str().map(str::to_string))
.unwrap_or_default();
Ok(MarkOutcome {
status: v["status"].as_str().unwrap_or("FAIL").to_string(),
plan,
posed: if posed.is_file() { Some(posed) } else { None },
etched: if have_etched { Some(etched) } else { None },
outlines,
summary,
hints,
})
}
/// Remove name variants an older thumbnailer wrote with fixed rotations (named90, named180, named
/// outlines from a previous run), so a stale wrong-way name can never be picked up.
pub fn clear_stale_named(chip: &ChipPaths) {
if let Ok(rd) = std::fs::read_dir(&chip.dir) {
for e in rd.flatten() {
let n = e.file_name().to_string_lossy().to_string();
if !n.starts_with(&chip.mpn) {
continue;
}
let stale = (n.contains("-3d-outline-") && (n.contains("-named") || n.contains("named90") || n.contains("named180")))
|| n.contains("-3d-iso-named90")
|| n.ends_with("-3d-iso-named-debug.png");
if stale {
let _ = std::fs::remove_file(e.path());
}
}
}
}