← Commit history

Publish 1.0.2

John Lauer ·70357d1611 ·4mo ago ·parent d06fbb2
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"+  } }\ No newline at end of file
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{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")+}++/// (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(())+}