app
Adom Symbol
Public Made by Adomby adom
KiCad symbol creator with interactive viewer and delivery to KiCad/Fusion 360
← Commit history
Publish 1.0.2
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"+ } }
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{'&':'&','<':'<','>':'>','"':'"'}[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",