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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
//! Locate chip-fetcher library entries on disk and inspect their files.
//!
//! chip-fetcher stores each chip at:
//! /home/adom/project/chip-fetcher/library/<MPN>/
//! with files named <MPN>.{step,kicad_sym,kicad_mod,pdf,...}.
//!
//! This module finds the library root and surfaces per-chip metadata
//! about which source files exist (so the renderers can decide which
//! thumbnails are even possible).
use anyhow::{anyhow, Result};
use std::path::{Path, PathBuf};
/// Marker file written by chip-fetcher after a successful import. Presence
/// means "thumbnails are out of date; re-render." chip-thumbnailer deletes
/// the marker after a successful render.
pub const THUMB_PENDING_MARKER: &str = ".thumb-pending";
/// Errors-from-last-render sidecar so chip-thumbnailer can resume failed
/// renders without re-running the whole pipeline.
pub const THUMB_ERRORS_FILE: &str = ".thumb-errors.json";
pub fn root() -> PathBuf {
if let Ok(p) = std::env::var("CHIP_FETCHER_LIBRARY") {
return PathBuf::from(p);
}
PathBuf::from("/home/adom/project/chip-fetcher/library")
}
#[derive(Debug, Clone)]
pub struct ChipPaths {
pub mpn: String,
pub dir: PathBuf,
pub kicad_sym: Option<PathBuf>,
pub kicad_mod: Option<PathBuf>,
pub step: Option<PathBuf>,
pub thumb_pending: bool,
}
impl ChipPaths {
pub fn for_mpn(mpn: &str) -> Result<Self> {
let dir = root().join(mpn);
if !dir.is_dir() {
return Err(anyhow!("chip not in library: {}", dir.display()));
}
let pick = |ext: &str| {
let p = dir.join(format!("{mpn}.{ext}"));
if p.is_file() {
Some(p)
} else {
None
}
};
Ok(Self {
mpn: mpn.to_string(),
kicad_sym: pick("kicad_sym"),
kicad_mod: pick("kicad_mod"),
step: pick("step").or_else(|| pick("stp")),
thumb_pending: dir.join(THUMB_PENDING_MARKER).exists(),
dir,
})
}
/// Build a ChipPaths for an arbitrary working directory (NOT under the
/// chip-fetcher library root). Used by `chip-thumbnailer serve` when a
/// user uploads a lone STEP file: we stage it in a per-session scratch
/// dir as `<name>.step` and render every 3D orientation into that dir.
/// Symbol / footprint stay `None` unless a matching `.kicad_sym` /
/// `.kicad_mod` was staged alongside it.
pub fn for_dir(dir: PathBuf, mpn: &str) -> Self {
let pick = |ext: &str| {
let p = dir.join(format!("{mpn}.{ext}"));
if p.is_file() {
Some(p)
} else {
None
}
};
Self {
mpn: mpn.to_string(),
kicad_sym: pick("kicad_sym"),
kicad_mod: pick("kicad_mod"),
step: pick("step").or_else(|| pick("stp")),
thumb_pending: false,
dir,
}
}
pub fn symbol_thumb(&self) -> PathBuf {
self.dir.join(format!("{}-symbol.svg", self.mpn))
}
pub fn footprint_thumb(&self) -> PathBuf {
self.dir.join(format!("{}-footprint.svg", self.mpn))
}
/// Canonical 3D thumbnail (as-is orientation, medium / 320×240).
/// Kept un-suffixed so back-compat consumers still find it.
pub fn three_d_thumb(&self) -> PathBuf {
self.three_d_thumb_path(ThreeDOrientation::AsIs, ThreeDSize::Md)
}
/// 3D thumbnail at a specific size, as-is orientation.
pub fn three_d_thumb_size(&self, size: ThreeDSize) -> PathBuf {
self.three_d_thumb_path(ThreeDOrientation::AsIs, size)
}
/// 3D thumbnail at a specific (orientation, size).
/// As-is keeps the original un-suffixed name; yUp/zUp get a token.
pub fn three_d_thumb_path(&self, orient: ThreeDOrientation, size: ThreeDSize) -> PathBuf {
let osuffix = orient.file_suffix();
let ssuffix = size.suffix();
self.dir
.join(format!("{}-3d-iso{osuffix}{ssuffix}.png", self.mpn))
}
/// Transparent-background icon for one orientation (Md size, alpha-keyed
/// from the shaded iso). asIs keeps the canonical `<MPN>-3d-iso-icon.png`
/// name adom-symbol reads first; others get the orientation token.
pub fn three_d_icon_path(&self, orient: ThreeDOrientation) -> PathBuf {
let osuffix = orient.file_suffix();
self.dir
.join(format!("{}-3d-iso{osuffix}-icon.png", self.mpn))
}
/// Iterator over all three rendered 3D sizes (as-is orientation).
pub fn three_d_thumbs(&self) -> Vec<(ThreeDSize, PathBuf)> {
ThreeDSize::all()
.into_iter()
.map(|s| (s, self.three_d_thumb_size(s)))
.collect()
}
}
/// Up-axis interpretation applied to the source STEP before the 3D
/// render. Six face-up cube orientations cover every "which side is
/// up" question; the consumer (e.g. chip-fetcher's UI) picks the
/// right one for each chip.
///
/// Math + reference impls + per-format catalog:
/// `gallia/skills/up-axis-conventions/SKILL.md`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThreeDOrientation {
/// Identity. Source +Z maps to render +Z. STEP / KiCad / Solidworks
/// canonical — right for ~80% of chips.
AsIs,
/// `R_x(π)`. Source -Z maps to render +Z. Use when STEP was
/// authored upside-down in Z (rare).
ZDown,
/// `R_x(+π/2)`. Source +Y maps to render +Z. Canonical Y-up,
/// matches glTF / three.js / Blender / Adom-canonical-viewer
/// (`rotateYUpToZUp` uses Math.PI/2). Right for chips authored
/// Y-up — Bosch sensors, Espressif modules, Nordic WLCSPs, etc.
YUp,
/// `R_x(-π/2)`. Source -Y maps to render +Z. Inverted Y-up — a
/// chip authored "Y-down" (pins along +Y instead of -Y). WAGO
/// connectors are the canonical real-world example.
YDown,
/// `R_y(-π/2)`. Source +X maps to render +Z. Rare — some
/// industrial parts.
XUp,
/// `R_y(+π/2)`. Source -X maps to render +Z.
XDown,
}
impl ThreeDOrientation {
pub fn all() -> [ThreeDOrientation; 6] {
[
ThreeDOrientation::AsIs,
ThreeDOrientation::ZDown,
ThreeDOrientation::YUp,
ThreeDOrientation::YDown,
ThreeDOrientation::XUp,
ThreeDOrientation::XDown,
]
}
/// Filename token appended between `-3d-iso` and the size suffix.
/// AsIs is empty (canonical un-suffixed name for back-compat).
pub fn file_suffix(self) -> &'static str {
match self {
ThreeDOrientation::AsIs => "",
ThreeDOrientation::ZDown => "-zdown",
ThreeDOrientation::YUp => "-yup",
ThreeDOrientation::YDown => "-ydown",
ThreeDOrientation::XUp => "-xup",
ThreeDOrientation::XDown => "-xdown",
}
}
/// Label used in the JSON manifest.
pub fn json_key(self) -> &'static str {
match self {
ThreeDOrientation::AsIs => "asIs",
ThreeDOrientation::ZDown => "zDown",
ThreeDOrientation::YUp => "yUp",
ThreeDOrientation::YDown => "yDown",
ThreeDOrientation::XUp => "xUp",
ThreeDOrientation::XDown => "xDown",
}
}
/// Value passed to `step2glb thumbnail --up-axis ...`.
pub fn step2glb_arg(self) -> &'static str {
match self {
ThreeDOrientation::AsIs => "asIs",
ThreeDOrientation::ZDown => "zDown",
ThreeDOrientation::YUp => "yUp",
ThreeDOrientation::YDown => "yDown",
ThreeDOrientation::XUp => "xUp",
ThreeDOrientation::XDown => "xDown",
}
}
/// Human-readable label for picker UIs.
pub fn label(self) -> &'static str {
match self {
ThreeDOrientation::AsIs => "as-is (Z-up canonical)",
ThreeDOrientation::ZDown => "Z-down (R_x(π))",
ThreeDOrientation::YUp => "Y-up (R_x(+π/2))",
ThreeDOrientation::YDown => "Y-down (R_x(-π/2))",
ThreeDOrientation::XUp => "X-up (R_y(-π/2))",
ThreeDOrientation::XDown => "X-down (R_y(+π/2))",
}
}
}
/// Standard 3D thumbnail sizes. `Md` is the canonical filename
/// (no suffix) — kept for back-compat with chip-thumbnailer 0.1.x
/// and downstream consumers that expect `<MPN>-3d-iso.png`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThreeDSize {
Sm,
Md,
Lg,
}
impl ThreeDSize {
pub fn all() -> [ThreeDSize; 3] {
[ThreeDSize::Sm, ThreeDSize::Md, ThreeDSize::Lg]
}
/// (width, height)
pub fn dims(self) -> (u32, u32) {
match self {
ThreeDSize::Sm => (160, 120),
ThreeDSize::Md => (320, 240),
ThreeDSize::Lg => (800, 600),
}
}
/// Filename suffix appended after `-3d-iso` and before `.png`.
/// Md → "" (canonical, no suffix). Sm → "-sm". Lg → "-lg".
pub fn suffix(self) -> &'static str {
match self {
ThreeDSize::Sm => "-sm",
ThreeDSize::Md => "",
ThreeDSize::Lg => "-lg",
}
}
pub fn label(self) -> &'static str {
match self {
ThreeDSize::Sm => "sm",
ThreeDSize::Md => "md",
ThreeDSize::Lg => "lg",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"sm" | "small" => Some(ThreeDSize::Sm),
"md" | "medium" => Some(ThreeDSize::Md),
"lg" | "large" => Some(ThreeDSize::Lg),
_ => None,
}
}
}
/// Walk the library and return every MPN dir.
pub fn list_mpns() -> Result<Vec<String>> {
let root = root();
if !root.exists() {
return Ok(vec![]);
}
let mut out = Vec::new();
for entry in std::fs::read_dir(&root)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
if let Some(name) = entry.file_name().to_str() {
if !name.starts_with('.') {
out.push(name.to_string());
}
}
}
}
out.sort();
Ok(out)
}
pub fn pending_chips() -> Result<Vec<String>> {
let mut out = Vec::new();
for mpn in list_mpns()? {
if root().join(&mpn).join(THUMB_PENDING_MARKER).exists() {
out.push(mpn);
}
}
Ok(out)
}
pub fn touch_pending(mpn: &str) -> Result<()> {
let path = root().join(mpn).join(THUMB_PENDING_MARKER);
std::fs::write(&path, b"").map_err(|e| anyhow!("touch {}: {e}", path.display()))
}
pub fn clear_pending(mpn: &str) -> Result<()> {
let path = root().join(mpn).join(THUMB_PENDING_MARKER);
if path.exists() {
std::fs::remove_file(&path).map_err(|e| anyhow!("rm {}: {e}", path.display()))?;
}
Ok(())
}
pub fn write_errors(mpn: &str, kinds: &[(&str, String)]) -> Result<PathBuf> {
let path = root().join(mpn).join(THUMB_ERRORS_FILE);
let mut obj = serde_json::Map::new();
for (k, v) in kinds {
obj.insert((*k).to_string(), serde_json::Value::String(v.clone()));
}
let blob = serde_json::Value::Object(obj);
std::fs::write(&path, serde_json::to_vec_pretty(&blob)?)?;
Ok(path)
}
pub fn clear_errors(mpn: &str) {
let _ = std::fs::remove_file(root().join(mpn).join(THUMB_ERRORS_FILE));
}
pub fn relative_to_workspace(p: &Path) -> String {
p.strip_prefix("/home/adom/project")
.map(|x| format!("/home/adom/project/{}", x.display()))
.unwrap_or_else(|_| p.display().to_string())
}