← Commit history

Publish 1.0.2

John Lauer ·e67641fb1d ·4mo ago ·parent 934f82b
5 files changed +158−8
bin/adom-symbol
⋯ 1 unchanged line ⋯
package.json+3−3
@@ -1,8 +1,8 @@ {   "slug": "adom-symbol",-  "version": "1.0.1",+  "version": "1.0.2",   "type": "app",-  "description": "A live, AI-drivable KiCad schematic-symbol studio \u2014 pick the source, lay out the pins, emboss the chip's real 3D model onto the symbol, and send to KiCad/Fusion/Altium/OrCAD.",+  "description": "A live, AI-drivable KiCad schematic-symbol studio with an interactive HTML embed export for wiki component pages.",   "dependencies": {},   "tags": [     "eda",@@ -15,7 +15,7 @@   "hero": {     "type": "billboard",     "headline": "Symbols, studio-grade.",-    "subhead": "Pick the source, lay out the pins, and emboss the chip's real 3D model onto the symbol \u2014 then send to KiCad, Fusion, Altium or OrCAD.",+    "subhead": "Lay out the pins, emboss the chip's real 3D model \u2014 then export an interactive embed for any page.",     "screenshot": "screenshots/shot.png",     "accent": "#00b8b1"   },
page.json+4−5
@@ -1,8 +1,8 @@ {   "slug": "adom-symbol",-  "version": "1.0.1",+  "version": "1.0.2",   "type": "app",-  "description": "A live, AI-drivable KiCad schematic-symbol studio — pick the source, lay out the pins, emboss the chip's real 3D model onto the symbol, and send to KiCad/Fusion/Altium/OrCAD.",+  "description": "A live, AI-drivable KiCad schematic-symbol studio with an interactive HTML embed export for wiki component pages.",   "dependencies": {},   "tags": [     "eda",@@ -15,7 +15,7 @@   "hero": {     "type": "billboard",     "headline": "Symbols, studio-grade.",-    "subhead": "Pick the source, lay out the pins, and emboss the chip's real 3D model onto the symbol — then send to KiCad, Fusion, Altium or OrCAD.",+    "subhead": "Lay out the pins, emboss the chip's real 3D model — then export an interactive embed for any page.",     "screenshot": "screenshots/shot.png",     "accent": "#00b8b1"   },@@ -28,6 +28,5 @@   "author": {     "name": "John Lauer",     "email": "[email protected]"-  },-  "title": "Adom Symbol"+  } }\ No newline at end of file
src/embed.htmladded+101
@@ -0,0 +1,101 @@+<!doctype html><html><head><meta charset="utf-8">+<title>__NAME__ — adom-symbol</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:#00e6dc}+  *{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}+  main{flex:1;display:flex;min-height:0}+  .vpwrap{flex:1;position:relative;min-width:0}+  .vp{position:absolute;inset:0;overflow:hidden;cursor:grab;background:+      radial-gradient(circle at 50% 40%, #121a26 0%, #0d1117 70%)}+  .vp.grab{cursor:grabbing}+  .vp svg{position:absolute;left:0;top:0;width:100%;height:100%;transform-origin:0 0;display:block}+  .tools{position:absolute;left:10px;top:10px;display:flex;gap:6px;z-index:5}+  .tools button{width:30px;height:30px;border-radius:8px;border:1px solid var(--border);background:var(--panel2);color:#cfd8e2;font-size:16px;cursor:pointer;display:grid;place-items:center}+  .tools button:hover{border-color:var(--accent);color:var(--accent)}+  .hint{position:absolute;right:10px;bottom:9px;color:var(--muted);font-size:11px;z-index:5;pointer-events:none}+  aside{flex:0 0 268px;border-left:1px solid var(--border);background:var(--panel);display:flex;flex-direction:column;min-height:0}+  aside .ah{padding:9px 12px;border-bottom:1px solid var(--border);font-size:12px;color:var(--muted);display:flex;align-items:center;gap:8px}+  aside input{flex:1;background:var(--panel2);border:1px solid var(--border);border-radius:7px;color:var(--text);font-size:12px;padding:5px 8px;outline:none}+  aside input:focus{border-color:var(--accent)}+  .pins{flex:1;overflow:auto}+  .pin{padding:7px 12px;border-bottom:1px solid #1a212c;cursor:pointer}+  .pin:hover{background:var(--panel2)}+  .pin.on{background:#15202e}+  .pin .row{display:flex;align-items:center;gap:8px}+  .pin .num{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:11px;color:#8fa0b4;min-width:22px;text-align:right}+  .pin .nme{font-weight:600;font-size:13px}+  .pin .ty{margin-left:auto;font-size:10px;padding:1px 6px;border-radius:5px;border:1px solid;white-space:nowrap}+  .pin .d{font-size:11.5px;color:#aeb6c2;line-height:1.45;margin:5px 0 1px 30px;display:none}+  .pin.on .d{display:block}+  .empty{color:var(--muted);font-size:12px;padding:14px}+  @media(max-width:620px){aside{flex-basis:200px}}+</style></head>+<body>+  <header><span class="dot"></span><span class="b">adom-symbol <span>· interactive</span></span><span class="nm">__NAME__</span></header>+  <main>+    <div class="vpwrap">+      <div class="vp" id="vp">+        <div class="tools">+          <button id="zin" title="Zoom in">+</button>+          <button id="zout" title="Zoom out">−</button>+          <button id="fit" title="Fit">⤢</button>+        </div>+        <div class="hint">drag to pan · scroll to zoom</div>+        __SVG__+      </div>+    </div>+    <aside>+      <div class="ah"><input id="q" placeholder="Filter pins…"></div>+      <div class="pins" id="pins"></div>+    </aside>+  </main>+<script>+var PINS=__PINS__;+(function(){+  var vp=document.getElementById('vp'), svg=vp.querySelector('svg');+  if(svg){ svg.removeAttribute('width'); svg.removeAttribute('height'); }+  var scale=1,tx=0,ty=0;+  function apply(){ if(svg) svg.style.transform='translate('+tx+'px,'+ty+'px) scale('+scale+')'; }+  function zoomAt(cx,cy,f){ tx=cx-(cx-tx)*f; ty=cy-(cy-ty)*f; scale*=f; apply(); }+  function fit(){ scale=1;tx=0;ty=0;apply(); }+  vp.addEventListener('wheel',function(e){ e.preventDefault(); var r=vp.getBoundingClientRect();+    zoomAt(e.clientX-r.left,e.clientY-r.top, e.deltaY<0?1.12:1/1.12); },{passive:false});+  var drag=false,lx=0,ly=0;+  vp.addEventListener('pointerdown',function(e){ drag=true;lx=e.clientX;ly=e.clientY;vp.classList.add('grab');vp.setPointerCapture(e.pointerId); });+  vp.addEventListener('pointermove',function(e){ if(!drag)return; tx+=e.clientX-lx;ty+=e.clientY-ly;lx=e.clientX;ly=e.clientY;apply(); });+  vp.addEventListener('pointerup',function(e){ drag=false;vp.classList.remove('grab'); });+  document.getElementById('zin').onclick=function(){ var r=vp.getBoundingClientRect();zoomAt(r.width/2,r.height/2,1.2); };+  document.getElementById('zout').onclick=function(){ var r=vp.getBoundingClientRect();zoomAt(r.width/2,r.height/2,1/1.2); };+  document.getElementById('fit').onclick=fit;+  // pin panel+  var box=document.getElementById('pins');+  function render(filter){+    box.innerHTML='';+    var f=(filter||'').toLowerCase();+    var shown=PINS.filter(function(p){ return !f || (p.name+' '+p.number+' '+p.type).toLowerCase().indexOf(f)>=0; });+    if(!shown.length){ box.innerHTML='<div class="empty">No pins match.</div>'; return; }+    shown.forEach(function(p){+      var el=document.createElement('div'); el.className='pin';+      var col=p.color||'#8b949e';+      el.innerHTML='<div class="row"><span class="num">'+esc(p.number)+'</span>'+        +'<span class="nme">'+esc(p.name)+'</span>'+        +'<span class="ty" style="color:'+col+';border-color:'+col+'66">'+esc(p.type||'')+'</span></div>'+        +(p.desc?'<div class="d">'+esc(p.desc)+'</div>':'');+      el.onclick=function(){ el.classList.toggle('on'); };+      box.appendChild(el);+    });+  }+  function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }+  document.getElementById('q').addEventListener('input',function(e){ render(e.target.value); });+  render('');+  apply();+})();+</script>+</body></html>
src/main.rs+50
@@ -79,6 +79,20 @@ enum Cmd {         #[arg(long)]         sym: Option<PathBuf>,     },+    /// Export a self-contained, INTERACTIVE HTML package (pan/zoom + a pin info+    /// panel with datasheet descriptions) for embedding in an iframe — e.g. a+    /// wiki component page. No server, no external assets: one portable .html.+    Embed {+        /// Path to a .kicad_sym file+        #[arg(long)]+        file: PathBuf,+        /// Symbol name (defaults to the first symbol in the file)+        #[arg(long)]+        name: Option<String>,+        /// Output .html path (default: <name>-embed.html next to the input)+        #[arg(long)]+        out: Option<PathBuf>,+    }, }  // ── create input (matches the Node /sym/create body) ──@@ -171,6 +185,7 @@ fn run() -> Result<()> {             Ok(())         }         Cmd::RenderDs2sf { file, out, layout, sym } => cmd_render_ds2sf(file, out, layout, sym),+        Cmd::Embed { file, name, out } => cmd_embed(file, name, out),         Cmd::Preview { file, name, out } => cmd_preview(file, name, out),         Cmd::Serve { port, file, name } => {             if let Some(f) = file {@@ -312,6 +327,41 @@ fn render_and_view(kicad_sym: &str, symbol_name: &str, folder: &std::path::Path,     Ok((svg, html_path)) } +fn cmd_embed(file: PathBuf, name: Option<String>, out: Option<PathBuf>) -> Result<()> {+    let content = fs::read_to_string(&file).with_context(|| format!("reading {}", file.display()))?;+    let symbol_name = name.unwrap_or_else(|| first_symbol_name(&content).unwrap_or_else(|| "symbol".to_string()));+    let svg = render::render_symbol_svg(&content, &symbol_name)?;+    let meta = parse::parse_kicad_sym(&content, &symbol_name);+    let html = build_embed_html(&symbol_name, &svg, &meta);+    let out = out.unwrap_or_else(|| {+        let folder = file.parent().map(|p| p.to_path_buf()).unwrap_or_else(|| PathBuf::from("."));+        folder.join(format!("{symbol_name}-embed.html"))+    });+    fs::write(&out, &html).with_context(|| format!("writing {}", out.display()))?;+    println!("OK: interactive embed ({} pins) → {}", meta.pins.len(), out.display());+    Ok(())+}++/// Build a self-contained, INTERACTIVE HTML viewer (pan/zoom + a filterable pin+/// panel with datasheet descriptions) ready to drop into an `<iframe>`.+pub(crate) fn build_embed_html(name: &str, svg: &str, meta: &parse::SymbolMeta) -> String {+    let pins: Vec<serde_json::Value> = meta+        .pins+        .iter()+        .map(|p| {+            serde_json::json!({+                "number": p.number, "name": p.name, "type": p.r#type,+                "color": p.color, "desc": p.desc,+            })+        })+        .collect();+    let pins_json = serde_json::to_string(&pins).unwrap_or_else(|_| "[]".into());+    include_str!("embed.html")+        .replace("__NAME__", &esc(name))+        .replace("__SVG__", svg)+        .replace("__PINS__", &pins_json)+}+ fn fetch_lib_from_service(library: &str) -> Result<String> {     let url = format!(         "{}/symbols/{}.kicad_sym",