app
Adom Library
Public Made by Adomby adom
adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.
← Commit history
Publish 1.0.2
6 files changed
+223−12
bin/adom-lbr⋯ 1 unchanged line ⋯
package.json+5−5@@ -1,8 +1,8 @@ { "slug": "adom-lbr",- "version": "1.0.1",+ "version": "1.0.2", "type": "app",- "description": "An EAGLE .lbr generator, linter and converter from KiCad sources \u2014 delivers to KiCad, Fusion 360, Altium and OrCAD. Standalone Rust, no local EAGLE.",+ "description": "EAGLE .lbr generator + linter for KiCad/Fusion/Altium/OrCAD, with a Fusion-style 3-column viewer you can embed in a page.", "dependencies": {}, "tags": [ "eda",@@ -10,12 +10,12 @@ "eagle", "lbr", "fusion360",- "library"+ "viewer" ], "hero": { "type": "billboard",- "headline": "One part. Every EDA tool.",- "subhead": "KiCad symbol + footprint to a true EAGLE .lbr \u2014 linted and delivered to KiCad, Fusion 360, Altium and OrCAD.",+ "headline": "See the part, three ways.",+ "subhead": "Symbol \u00b7 footprint \u00b7 3D side by side, with the pin\u2192pad map and datasheet descriptions \u2014 exportable as one embeddable file.", "screenshot": "screenshots/shot.png", "accent": "#00b8b1" },
page.json+6−7@@ -1,8 +1,8 @@ { "slug": "adom-lbr",- "version": "1.0.1",+ "version": "1.0.2", "type": "app",- "description": "An EAGLE .lbr generator, linter and converter from KiCad sources — delivers to KiCad, Fusion 360, Altium and OrCAD. Standalone Rust, no local EAGLE.",+ "description": "EAGLE .lbr generator + linter for KiCad/Fusion/Altium/OrCAD, with a Fusion-style 3-column viewer you can embed in a page.", "dependencies": {}, "tags": [ "eda",@@ -10,12 +10,12 @@ "eagle", "lbr", "fusion360",- "library"+ "viewer" ], "hero": { "type": "billboard",- "headline": "One part. Every EDA tool.",- "subhead": "KiCad symbol + footprint to a true EAGLE .lbr — linted and delivered to KiCad, Fusion 360, Altium and OrCAD.",+ "headline": "See the part, three ways.",+ "subhead": "Symbol · footprint · 3D side by side, with the pin→pad map and datasheet descriptions — exportable as one embeddable file.", "screenshot": "screenshots/shot.png", "accent": "#00b8b1" },@@ -28,6 +28,5 @@ "author": { "name": "John Lauer", "email": "[email protected]"- },- "title": "Adom Library"+ } }
screenshots/shot.png⋯ 1 unchanged line ⋯
src/embed.htmladded+81@@ -0,0 +1,81 @@+<!doctype html><html><head><meta charset="utf-8">+<title>__NAME__ — adom-lbr</title>+<meta name="viewport" content="width=device-width,initial-scale=1">+<style>+ :root{--bg:#0d1117;--panel:#11161f;--panel2:#161c27;--border:#222b38;--text:#e6edf3;--muted:#8b949e;--accent:#5fe3c0}+ *{box-sizing:border-box;margin:0;padding:0}html,body{height:100%}+ body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;display:flex;flex-direction:column;overflow:hidden}+ header{display:flex;align-items:center;gap:9px;padding:9px 14px;border-bottom:1px solid var(--border);flex:0 0 auto}+ header .dot{width:9px;height:9px;border-radius:50%;background:var(--accent);box-shadow:0 0 10px var(--accent)}+ header .b{font-weight:700;font-size:14px}header .b span{color:var(--muted);font-weight:400}+ header .nm{margin-left:auto;font-weight:600;font-size:13px;color:#cfd8e2}+ .cols{flex:1 1 56%;display:flex;min-height:0;border-bottom:1px solid var(--border)}+ .col{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid var(--border)}+ .col:last-child{border-right:0}+ .col .ch{padding:7px 11px;font-size:11px;letter-spacing:.4px;text-transform:uppercase;color:var(--muted);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:7px}+ .col .ch .n{margin-left:auto;text-transform:none;letter-spacing:0;color:#7e8a99}+ .vp{flex:1;position:relative;overflow:hidden;cursor:grab;background:radial-gradient(circle at 50% 42%,#121a26 0%,#0d1117 72%)}+ .vp.grab{cursor:grabbing}+ .vp svg,.vp img{position:absolute;left:0;top:0;width:100%;height:100%;transform-origin:0 0;display:block;object-fit:contain}+ .vp .tools{position:absolute;left:8px;top:8px;display:flex;gap:5px;z-index:5}+ .vp .tools button{width:26px;height:26px;border-radius:7px;border:1px solid var(--border);background:var(--panel2);color:#cfd8e2;font-size:14px;cursor:pointer}+ .vp .tools button:hover{border-color:var(--accent);color:var(--accent)}+ .map{flex:1 1 44%;overflow:auto;background:var(--panel)}+ .map .mh{position:sticky;top:0;background:var(--panel);padding:8px 13px;font-size:12px;color:var(--muted);border-bottom:1px solid var(--border);z-index:2}+ table{width:100%;border-collapse:collapse;font-size:12.5px}+ th,td{text-align:left;padding:7px 13px;border-bottom:1px solid #1a212c;vertical-align:top}+ th{position:sticky;top:37px;background:var(--panel2);color:#9fb0c0;font-size:11px;text-transform:uppercase;letter-spacing:.4px;z-index:1}+ td.pin{font-weight:600}td.pad{font-family:'JetBrains Mono',ui-monospace,monospace;color:#7ef0d2}+ td.arrow{color:#3a4757;text-align:center;width:18px}+ .ty{font-size:10px;padding:1px 6px;border-radius:5px;border:1px solid;white-space:nowrap}+ td.d{color:#aeb6c2;line-height:1.45;max-width:520px}+ tr:hover td{background:#15202e}+ .ph{display:grid;place-items:center;height:100%;color:#4b5666;font-size:12px;text-align:center;padding:18px}+</style></head>+<body>+ <header><span class="dot"></span><span class="b">adom-lbr <span>· library viewer</span></span><span class="nm">__NAME__</span></header>+ <div class="cols">+ <div class="col"><div class="ch">Symbol</div><div class="vp" data-vp>__SYM__</div></div>+ <div class="col"><div class="ch">Footprint <span class="n">__PADN__ pads</span></div><div class="vp" data-vp>__FP__</div></div>+ <div class="col"><div class="ch">3D</div><div class="vp" data-vp>__THREED__</div></div>+ </div>+ <div class="map">+ <div class="mh">Pin → pad mapping <span style="color:#7e8a99">· __PINN__ pins, with datasheet descriptions</span></div>+ <table><thead><tr><th>Pin</th><th>Name</th><th></th><th>Pad</th><th>Type</th><th>Description</th></tr></thead>+ <tbody id="rows"></tbody></table>+ </div>+<script>+var PINS=__PINS__;+(function(){+ // each viewport: pan/zoom on its <svg> or <img>+ document.querySelectorAll('[data-vp]').forEach(function(vp){+ var el=vp.querySelector('svg,img'); if(!el) return;+ if(el.tagName.toLowerCase()==='svg'){ el.removeAttribute('width'); el.removeAttribute('height'); }+ var s=1,tx=0,ty=0;+ function ap(){ el.style.transform='translate('+tx+'px,'+ty+'px) scale('+s+')'; }+ function zoom(cx,cy,f){ tx=cx-(cx-tx)*f; ty=cy-(cy-ty)*f; s*=f; ap(); }+ var t=document.createElement('div'); t.className='tools';+ t.innerHTML='<button data-z="1.2">+</button><button data-z="0.833">−</button><button data-f>⤢</button>';+ vp.appendChild(t);+ t.querySelectorAll('button').forEach(function(btn){ btn.onclick=function(){ var r=vp.getBoundingClientRect();+ if(btn.hasAttribute('data-f')){ s=1;tx=0;ty=0;ap(); } else { zoom(r.width/2,r.height/2,parseFloat(btn.dataset.z)); } }; });+ vp.addEventListener('wheel',function(e){ e.preventDefault(); var r=vp.getBoundingClientRect(); zoom(e.clientX-r.left,e.clientY-r.top,e.deltaY<0?1.12:1/1.12); },{passive:false});+ var d=false,lx,ly;+ vp.addEventListener('pointerdown',function(e){ if(e.target.closest('.tools'))return; d=true;lx=e.clientX;ly=e.clientY;vp.classList.add('grab');vp.setPointerCapture(e.pointerId); });+ vp.addEventListener('pointermove',function(e){ if(!d)return; tx+=e.clientX-lx;ty+=e.clientY-ly;lx=e.clientX;ly=e.clientY;ap(); });+ vp.addEventListener('pointerup',function(){ d=false;vp.classList.remove('grab'); });+ ap();+ });+ function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&','<':'<','>':'>','"':'"'}[c];}); }+ var rows=document.getElementById('rows');+ PINS.forEach(function(p){+ var col=p.color||'#8b949e', tr=document.createElement('tr');+ tr.innerHTML='<td class="pin">'+esc(p.number)+'</td><td class="pin">'+esc(p.name)+'</td>'+ +'<td class="arrow">→</td><td class="pad">'+esc(p.pad)+'</td>'+ +'<td><span class="ty" style="color:'+col+';border-color:'+col+'66">'+esc(p.type||'')+'</span></td>'+ +'<td class="d">'+esc(p.desc||'')+'</td>';+ rows.appendChild(tr);+ });+})();+</script>+</body></html>
src/main.rs+131@@ -60,6 +60,18 @@ enum Commands { /// Path to the .kicad_sym file path: PathBuf, },+ /// Export a Fusion-style 3-column INTERACTIVE HTML package — symbol | footprint+ /// | 3D, each pan/zoomable, with a pin→pad mapping table carrying the datasheet+ /// descriptions below. Self-contained for embedding in an iframe (e.g. a wiki+ /// component page). Assembles from a chip directory's renders + extracted data.+ Embed {+ /// Chip directory (a chip-fetcher library entry) to assemble the viewer from+ #[arg(long)]+ dir: PathBuf,+ /// Output .html path (default: <mpn>-lbr-embed.html in the dir)+ #[arg(long)]+ out: Option<PathBuf>,+ }, /// Run the live app server (AI-drivable: /state, /load, /eval, /console) Serve { /// Port to listen on@@ -962,6 +974,12 @@ fn main() { } } + Commands::Embed { dir, out } => {+ if let Err(e) = cmd_embed(&dir, out) {+ eprintln!("ERROR: {e:#}");+ process::exit(1);+ }+ } Commands::Serve { port, file } => { if let Some(f) = file { match fs::read_to_string(&f) {@@ -997,3 +1015,116 @@ fn main() { } } }++// ── `embed`: Fusion-style 3-column interactive HTML package ──++fn b64(data: &[u8]) -> String {+ const T: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";+ let mut out = String::with_capacity(data.len().div_ceil(3) * 4);+ for ch in data.chunks(3) {+ let b = [ch[0], *ch.get(1).unwrap_or(&0), *ch.get(2).unwrap_or(&0)];+ let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;+ out.push(T[(n >> 18 & 63) as usize] as char);+ out.push(T[(n >> 12 & 63) as usize] as char);+ out.push(if ch.len() > 1 { T[(n >> 6 & 63) as usize] as char } else { '=' });+ out.push(if ch.len() > 2 { T[(n & 63) as usize] as char } else { '=' });+ }+ out+}++fn esc_html(s: &str) -> String {+ s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)+}++/// (human label, hex color) for a ds2sf/kicad pin type token.+fn pin_type_style(t: &str) -> (&'static str, &'static str) {+ match t.to_lowercase().replace('-', "_").as_str() {+ "power_in" | "power_out" | "power" => ("Power", "#ef5350"),+ "input" | "in" => ("Input", "#26c6da"),+ "output" | "out" => ("Output", "#66bb6a"),+ "bidirectional" | "bidi" | "io" => ("Bidir", "#ab47bc"),+ "open_collector" | "open_emitter" => ("Open", "#ffa726"),+ "tri_state" => ("Tri-state", "#ffa726"),+ "passive" => ("Passive", "#90a4ae"),+ "unspecified" | "" => ("—", "#90a4ae"),+ "no_connect" | "nc" | "unconnected" => ("NC", "#5b6673"),+ _ => ("Signal", "#80cbc4"),+ }+}++fn read_first(dir: &std::path::Path, names: &[String]) -> Option<String> {+ for n in names {+ if let Ok(s) = fs::read_to_string(dir.join(n)) {+ return Some(s);+ }+ }+ None+}++fn cmd_embed(dir: &std::path::Path, out: Option<PathBuf>) -> Result<(), String> {+ let mpn = dir.file_name().and_then(|s| s.to_str()).ok_or("bad --dir")?.to_string();+ // renders+ let sym_svg = read_first(dir, &[format!("{mpn}-symbol.svg")])+ .map(|s| s)+ .unwrap_or_else(|| "<div class=\"ph\">no symbol render in this directory</div>".into());+ let fp_svg = read_first(dir, &[format!("{mpn}-footprint.authoritative.svg"), format!("{mpn}-footprint.svg")])+ .unwrap_or_else(|| "<div class=\"ph\">no footprint render in this directory</div>".into());+ let threed = {+ let cands = ["-3d-iso-named.png", "-3d-iso.png", "-3d-iso-lg.png"]+ .iter().map(|s| dir.join(format!("{mpn}{s}"))).find(|p| p.exists());+ match cands.and_then(|p| fs::read(&p).ok()) {+ Some(bytes) => format!("<img src=\"data:image/png;base64,{}\">", b64(&bytes)),+ None => "<div class=\"ph\">no 3D render in this directory</div>".into(),+ }+ };+ // optional pin→pad connects from a .lbr+ let mut pin_to_pad: std::collections::HashMap<String, String> = std::collections::HashMap::new();+ if let Some(lbr) = read_first(dir, &[format!("{mpn}.lbr")]) {+ if let Ok(re) = regex_lite::Regex::new(r#"<connect gate="[^"]*" pin="([^"]+)" pad="([^"]+)"/>"#) {+ for cap in re.captures_iter(&lbr) {+ pin_to_pad.insert(cap[1].to_string(), cap[2].to_string());+ }+ }+ }+ // pins + descriptions from the extracted symbol JSON+ let mut pins_json = String::from("[]");+ let mut pad_count = fp_svg.matches("adom-pad").count().max(0);+ if let Some(ext) = read_first(dir, &[format!("{mpn}-symbol.extracted.json")]) {+ if let Ok(v) = serde_json::from_str::<serde_json::Value>(&ext) {+ if let Some(arr) = v.get("pins").and_then(|p| p.as_array()) {+ let rows: Vec<serde_json::Value> = arr.iter().map(|p| {+ let num = p.get("number").and_then(|x| x.as_str()).unwrap_or("").to_string();+ let raw_t = p.get("type").and_then(|x| x.as_str()).unwrap_or("");+ let (label, color) = pin_type_style(raw_t);+ let pad = pin_to_pad.get(p.get("name").and_then(|x| x.as_str()).unwrap_or(""))+ .cloned().unwrap_or_else(|| num.clone());+ serde_json::json!({+ "number": num,+ "name": p.get("name").and_then(|x| x.as_str()).unwrap_or(""),+ "pad": pad,+ "type": label,+ "color": color,+ "desc": p.get("description").and_then(|x| x.as_str()).unwrap_or(""),+ })+ }).collect();+ pad_count = pad_count.max(rows.len());+ pins_json = serde_json::to_string(&rows).unwrap_or_else(|_| "[]".into());+ }+ }+ }+ let pin_count = serde_json::from_str::<serde_json::Value>(&pins_json).ok()+ .and_then(|v| v.as_array().map(|a| a.len())).unwrap_or(0);++ let html = include_str!("embed.html")+ .replace("__NAME__", &esc_html(&mpn))+ .replace("__SYM__", &sym_svg)+ .replace("__FP__", &fp_svg)+ .replace("__THREED__", &threed)+ .replace("__PINS__", &pins_json)+ .replace("__PADN__", &pad_count.to_string())+ .replace("__PINN__", &pin_count.to_string());+ let out = out.unwrap_or_else(|| dir.join(format!("{mpn}-lbr-embed.html")));+ fs::write(&out, &html).map_err(|e| format!("writing {}: {e}", out.display()))?;+ println!("OK: 3-column interactive embed ({pin_count} pins) → {}", out.display());+ Ok(())+}