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
2.12.4: demo transport + narration MP3s; captured KiCad evidence frames
8 files changed
+130−57
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.12.3"+version = "2.12.4" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.12.3"+version = "2.12.4" edition = "2021" description = "adom-lbr generator / EDA translator: the canonical adom-lbr JSON is the hub, with native converters to Altium (.SchLib/.PcbLib/.IntLib), EAGLE/Fusion (.lbr), and KiCad. Altium output is written natively — no Altium needed at conversion time."
VERSION+1−1@@ -1 +1 @@-2.12.3+2.12.4
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.12.3",+ "version": "2.12.4", "type": "app", "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad 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.", "dependencies": {},
page.json+9−12@@ -1,8 +1,8 @@ { "slug": "adom-lbr",- "version": "2.12.3",+ "version": "2.12.4", "type": "app",- "description": "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.",+ "description": "adom-lbr \u2014 the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON \u2014 symbol + footprint + 3D + pin\u2194pad 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.", "dependencies": {}, "tags": [ "eda",@@ -52,24 +52,21 @@ ], "files": [ "bin/**",- "assets/**", "SKILL.md",- "skills/**", "README.md", "install.sh", "uninstall.sh",- "screenshots/**",- "dockbar.json"+ "screenshots/**" ],- "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools — KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion — through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.",+ "discovery_pitch": "Convert electronic-component symbols + footprints between any two EDA tools \u2014 KiCad, Altium (written natively, no Altium needed), and EAGLE/Fusion \u2014 through one canonical adom-lbr JSON, silk/courtyard/fab layers preserved. Plus a Manager to browse and inspect any wiki part.", "sample_prompts": [ { "prompt": "Convert this KiCad symbol and footprint to an Altium SchLib and PcbLib",- "label": "KiCad → Altium"+ "label": "KiCad \u2192 Altium" }, { "prompt": "Import this Altium footprint into the adom-lbr JSON",- "label": "Altium → JSON"+ "label": "Altium \u2192 JSON" }, { "prompt": "Bundle this part into a single Altium IntLib",@@ -77,15 +74,15 @@ }, { "prompt": "Convert this Altium SchLib and PcbLib to a KiCad symbol and footprint",- "label": "Altium → KiCad"+ "label": "Altium \u2192 KiCad" }, { "prompt": "Export this adom-lbr JSON to a KiCad .kicad_sym and .kicad_mod",- "label": "JSON → KiCad"+ "label": "JSON \u2192 KiCad" }, { "prompt": "Open the adom-lbr Manager to browse wiki components and view the layer stackup", "label": "Browse parts" } ]-}+}
skills/adom-lbr-dockbar/SKILL.md+10−2@@ -129,8 +129,16 @@ live streaming console of each verb and its result, and evidence screenshots, matching ab's own web-control demo so the user is never running blind. Evidence is FINAL frames only, never a mid-operation screenshot (a step shot usually still shows the previous part and reads as "you opened the wrong-thing"; John, 2026-08-20). KiCad evidence comes from the show verb's own-reply, else `kicad_state {includeImages:true}`.+thing"; John, 2026-08-20). KiCad evidence uses the web-control shot contract+(adom-bridge#91): every show verb is called with `capture:true` and returns a+`thumbnailDataUrl` frame of the window it opened, canvas-paint-polled so a+blank can never pose as proof. The Library send opens ALL THREE surfaces+(symbol, footprint, 3D chip) and renders one captured frame of each - a+library IS all three. Frame titles are verified against the part name and+captioned "unverified" when they do not match (the 3D Viewer's own title is+accepted for the 3D frame). The Demo tour ships pre-rendered adom-tts+narration (assets/tour/step-NN.mp3, served at /tour/) with a floating+play/pause/prev/next/exit transport; nothing is synthesized at view time. Detection (`GET /health`) lights the toolbar's dot. It works when the page is served at its 127.0.0.1 address, which is exactly how the dock opens it; a
src/manage.html+94−39@@ -86,6 +86,13 @@ #tour-card button:hover{border-color:var(--accent)} #tour-card button.primary{background:var(--accent);color:var(--on-accent);border-color:var(--accent)} #tour-card button:active{transform:translateY(1px)}+ #tour-bar{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);z-index:3002;display:none;align-items:center;gap:6px;background:var(--elevated);border:1px solid var(--border);border-radius:999px;padding:6px 10px;box-shadow:0 12px 36px #000c}+ #tour-bar.show{display:flex}+ #tour-bar button{width:34px;height:34px;border-radius:50%;border:1px solid var(--border);background:#1a2130;color:var(--ink);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:border-color .12s,background .12s,transform .06s}+ #tour-bar button:hover{border-color:var(--accent);color:var(--accent)}+ #tour-bar button:active{transform:translateY(1px)}+ #tour-bar button.play{background:var(--accent);color:var(--on-accent);border-color:var(--accent)}+ #tour-bar .tn{font:600 11.5px var(--brand-body);color:var(--text-2);padding:0 6px;min-width:44px;text-align:center} #btnDemo{display:inline-flex;align-items:center;gap:6px;background:#12302f;border:1px solid var(--acc);color:var(--acc);border-radius:8px;padding:4px 12px;font:700 12px var(--brand-heading);cursor:pointer;transition:filter .12s,transform .06s} #btnDemo:hover{filter:brightness(1.2)} #btnDemo:active{transform:translateY(1px)}@@ -328,13 +335,14 @@ <div id="tour-ring"></div> <div id="tour-card" role="dialog" aria-labelledby="tour-title"> <h3><span class="step-n" id="tour-n"></span><span id="tour-title"></span></h3>- <p id="tour-text"></p>- <div class="row">- <button id="tour-back">Back</button>- <span class="spacer" id="tour-progress"></span>- <button id="tour-exit">Exit</button>- <button class="primary" id="tour-next">Next</button>- </div>+ <p id="tour-text" style="margin-bottom:0"></p>+ </div>+ <div id="tour-bar" role="toolbar" aria-label="Demo controls">+ <button id="tour-prev" title="Previous step (left arrow)" aria-label="Previous"><svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M6 6h2v12H6zm3.5 6 8.5 6V6z"/></svg></button>+ <button id="tour-play" class="play" title="Pause (space)" aria-label="Play or pause"><svg id="tour-play-ic" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg></button>+ <button id="tour-next" title="Next step (right arrow)" aria-label="Next"><svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M16 6h2v12h-2zM6 18l8.5-6L6 6z"/></svg></button>+ <span class="tn" id="tour-progress">1 / 13</span>+ <button id="tour-exit" title="Exit the demo (Esc)" aria-label="Exit"><svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg></button> </div> <dialog id="dlgSettings" class="settings" tabindex="-1" aria-labelledby="settingsTitle">@@ -978,9 +986,10 @@ function renderEvidence(frames,expectTokens,cmd){ let allVerified=true; frames.slice(0,4).forEach(x=>{ const title=x.title||x.step||'frame';- const hit=expectTokens.some(t=>t&&title.toLowerCase().includes(t.toLowerCase()));+ const unrendered=x.canvasRendered===false;+ const hit=!unrendered&&expectTokens.some(t=>t&&title.toLowerCase().includes(t.toLowerCase())); if(!hit)allVerified=false;- sp.shot(x.thumbnailDataUrl||x.image||x.base64||x.png,(hit?'':'⚠ unverified: ')+title);+ sp.shot(x.thumbnailDataUrl||x.image||x.base64||x.png,(unrendered?'⚠ canvas not painted: ':hit?'':'⚠ unverified: ')+title); }); if(allVerified)sp.log(' evidence: '+frames.length+' frame(s), titles name the part'); else sp.log(' WARNING: some evidence titles do not name this part; the editor may be showing something else');@@ -1013,32 +1022,47 @@ async function sendKicad(kind){ sp.bar(3);sp.step('preparing files…'); sp.log('GET api/export/'+currentPart); const k=await kicadInstall(currentPart);+ // capture:true is the designed web-control evidence contract (adom-bridge#91):+ // the verb screenshots the window it opened, polls until the canvas actually+ // painted, and returns thumbnailDataUrl frames a page can render directly.+ const CAP={capture:true,captureMaxWidth:640}; const shows={- 'kicad-symbol': ['kicad_show_symbol', {symbolName:k.symbol,libraryName:k.lib}],- 'kicad-footprint':['kicad_show_footprint',{footprintName:k.footprint,libraryName:k.lib,library:k.lib}],- 'kicad-3d': ['kicad_show_3d_chip', {footprintName:k.footprint,libraryName:k.lib,library:k.lib}],- 'kicad-library': ['kicad_show_library', {libraryName:k.lib,partName:k.symbol}],+ 'kicad-symbol': ['kicad_show_symbol', Object.assign({symbolName:k.symbol,libraryName:k.lib},CAP)],+ 'kicad-footprint':['kicad_show_footprint',Object.assign({footprintName:k.footprint,libraryName:k.lib,library:k.lib},CAP)],+ 'kicad-3d': ['kicad_show_3d_chip', Object.assign({footprintName:k.footprint,libraryName:k.lib,library:k.lib},CAP),['3D Viewer']], };- const [cmd,args]=shows[kind];- sp.bar(80);sp.step('opening in KiCad…');- sp.log(cmd+' '+JSON.stringify(args));- let r=await abVerb(cmd,args); const failed=x=>!(x.s===200&&x.j&&x.j.success!==false&&!x.j.errorCode);- if(failed(r)&&r.j&&(r.j.errorCode==='library_cache_stale'||String(r.j.error||'').includes('indexed its libraries'))){- // The open editor indexed its libraries before our install. The bridge's- // own remedy: rescan:true reopens the editor to re-index (~1 minute cold- // library scan; it never closes a HUMAN-opened editor without this).- sp.log(' editor library index is stale; reopening with rescan:true (the cold re-index takes about a minute)');- sp.step('re-indexing KiCad’s libraries (about a minute)…');sp.bar(null);- r=await abVerb(cmd,Object.assign({rescan:true},args));+ const bare=n=>String(n||'').split(':').pop();+ const expect=[bare(k.symbol),k.footprint,k.lib+':'];+ async function runShow(cmd,args,pct,extraExpect){+ sp.bar(pct);sp.step('opening in KiCad…');+ sp.log(cmd+' '+JSON.stringify(args));+ let r=await abVerb(cmd,args);+ if(failed(r)&&r.j&&(r.j.errorCode==='library_cache_stale'||String(r.j.error||'').includes('indexed its libraries'))){+ // Stale editor index: the bridge's own remedy is rescan:true (reopens+ // the editor; the cold re-index takes about a minute).+ sp.log(' editor library index is stale; reopening with rescan:true (about a minute)');+ sp.step('re-indexing KiCad’s libraries (about a minute)…');sp.bar(null);+ r=await abVerb(cmd,Object.assign({rescan:true},args));+ }+ if(failed(r)){+ if(r.j&&r.j._hint)sp.log(' hint: '+r.j._hint);+ throw new Error(cmd+': '+((r.j&&(r.j.error||r.j.errorCode))||r.s));+ }+ sp.log(' '+cmd+' ok'+((r.j&&r.j.autoRescanned)?' (editor re-indexed)':''));+ return renderEvidence(findShots(r.j),expect.concat(extraExpect||[]),cmd); }- if(failed(r)){- if(r.j&&r.j._hint)sp.log(' hint: '+r.j._hint);- throw new Error(cmd+': '+((r.j&&(r.j.error||r.j.errorCode))||r.s));+ let verified;+ if(kind==='kicad-library'){+ // A library IS a symbol + a footprint + a 3D model (John's doctrine), so+ // the Library send opens ALL THREE and renders one captured frame of each.+ verified =await runShow('kicad_show_symbol', Object.assign({symbolName:k.symbol,libraryName:k.lib},CAP),78);+ verified&=await runShow('kicad_show_footprint',Object.assign({footprintName:k.footprint,libraryName:k.lib,library:k.lib},CAP),86);+ verified&=await runShow('kicad_show_3d_chip', Object.assign({footprintName:k.footprint,libraryName:k.lib,library:k.lib},CAP),94,['3D Viewer']);+ } else {+ const [cmd,args,extra]=shows[kind];+ verified=await runShow(cmd,args,80,extra); }- sp.log(' '+cmd+' ok'+((r.j&&r.j.autoRescanned)?' (editor re-indexed)':''));- const bare=n=>String(n||'').split(':').pop();- const verified=renderEvidence(findShots(r.j),[bare(k.symbol),k.footprint,k.lib+':'],cmd); sp.bar(100);sp.step(verified?'done — open in KiCad':'done, but verify the KiCad window yourself'); toast('Open in KiCad: '+kind.replace('kicad-',''),false,verified?'success':undefined); }@@ -1132,11 +1156,23 @@ const TOUR=[ {sel:'#btnSettings',title:'Settings',text:'The cog holds the rest: the Say it strip switch, whose boards count as My projects, the list cache, and package Update/Uninstall with a live console. Every section explains itself.'}, {sel:'#say',title:'That is the tour',text:'From here: browse <b>Parts</b>, open anything, and when you want real work done, copy a phrase and ask your AI, the word is <b>lbr</b>. Enjoy.'} ];-let tourI=-1;+let tourI=-1,tourPlaying=false,tourTimer=null;+const tourAudio=new Audio();+tourAudio.preload='auto';+tourAudio.onended=()=>{ if(tourPlaying&&tourI>=0){ if(tourI<TOUR.length-1)tourShow(tourI+1); else tourStop(); } };+tourAudio.onerror=()=>{ if(tourPlaying&&tourI>=0){ clearTimeout(tourTimer); tourTimer=setTimeout(()=>{ if(tourPlaying&&tourI<TOUR.length-1)tourShow(tourI+1); else if(tourPlaying)tourStop(); },7000); } };+function tourPaintPlay(){+ const ic=document.getElementById('tour-play-ic');+ ic.innerHTML=tourPlaying?'<path fill="currentColor" d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>':'<path fill="currentColor" d="M8 5v14l11-7z"/>';+ document.getElementById('tour-play').title=tourPlaying?'Pause (space)':'Play (space)';+} function tourStop(){- tourI=-1;demoUnhover();+ tourI=-1;tourPlaying=false;clearTimeout(tourTimer);+ try{tourAudio.pause();}catch(e){}+ demoUnhover(); document.getElementById('tour-ring').style.display='none'; document.getElementById('tour-card').style.display='none';+ document.getElementById('tour-bar').classList.remove('show'); } async function tourShow(i){ const prev=TOUR[tourI];@@ -1158,10 +1194,19 @@ async function tourShow(i){ document.getElementById('tour-n').textContent=(i+1)+' / '+TOUR.length; document.getElementById('tour-title').textContent=st.title; document.getElementById('tour-text').innerHTML=st.text;- document.getElementById('tour-progress').textContent='';- document.getElementById('tour-back').style.visibility=i===0?'hidden':'visible';- document.getElementById('tour-next').textContent=i===TOUR.length-1?'Done':'Next';+ document.getElementById('tour-progress').textContent=(i+1)+' / '+TOUR.length; card.style.display='block';+ document.getElementById('tour-bar').classList.add('show');+ tourPaintPlay();+ // pre-rendered narration (adom-tts, shipped with the app - nothing generated live)+ clearTimeout(tourTimer);+ try{tourAudio.pause();}catch(e){}+ tourAudio.src='tour/step-'+String(i+1).padStart(2,'0')+'.mp3';+ if(tourPlaying){+ tourAudio.play().catch(()=>{ // autoplay refused: keep the tour moving on a timer+ tourTimer=setTimeout(()=>{ if(tourPlaying&&tourI<TOUR.length-1)tourShow(tourI+1); },7000);+ });+ } // place the card near the ring, clamped on screen, never covering it const rr=el2?el2.getBoundingClientRect():{left:innerWidth/2-200,top:innerHeight/2,bottom:innerHeight/2,width:0}; const cw=Math.min(400,innerWidth*0.9),ch=card.offsetHeight||170;@@ -1170,11 +1215,21 @@ async function tourShow(i){ if(y+ch>innerHeight-12)y=Math.max(12,rr.top-ch-14); card.style.left=x+'px';card.style.top=y+'px'; }-document.getElementById('btnDemo').onclick=()=>tourShow(0);-document.getElementById('tour-next').onclick=()=>tourShow(tourI+1);-document.getElementById('tour-back').onclick=()=>tourShow(tourI-1);+document.getElementById('btnDemo').onclick=()=>{tourPlaying=true;tourShow(0);};+document.getElementById('tour-next').onclick=()=>{ if(tourI<TOUR.length-1)tourShow(tourI+1); else tourStop(); };+document.getElementById('tour-prev').onclick=()=>{ if(tourI>0)tourShow(tourI-1); };+document.getElementById('tour-play').onclick=()=>{+ tourPlaying=!tourPlaying;tourPaintPlay();+ if(tourPlaying){ (tourAudio.src&&tourAudio.paused)?tourAudio.play().catch(()=>{}):tourShow(tourI); }+ else { try{tourAudio.pause();}catch(e){} clearTimeout(tourTimer); }+}; document.getElementById('tour-exit').onclick=tourStop;-document.addEventListener('keydown',e=>{if(tourI>=0){if(e.key==='Escape')tourStop();if(e.key==='ArrowRight'||e.key==='Enter')tourShow(tourI+1);if(e.key==='ArrowLeft')tourShow(tourI-1);}});+document.addEventListener('keydown',e=>{if(tourI>=0){+ if(e.key==='Escape')tourStop();+ if(e.key==='ArrowRight'||e.key==='Enter'){e.preventDefault();document.getElementById('tour-next').click();}+ if(e.key==='ArrowLeft'){e.preventDefault();document.getElementById('tour-prev').click();}+ if(e.key===' '){e.preventDefault();document.getElementById('tour-play').click();}+}}); addEventListener('resize',()=>{if(tourI>=0)tourShow(tourI);}); document.getElementById('btn-update').onclick=()=>runJob('update');
src/manage.rs+13@@ -138,6 +138,19 @@ fn route_inner(method: &Method, url: &str, path: &str, body: &str) -> Response<C (Method::Post, "/api/apply") => json_resp(&start_apply(body).to_string()), (Method::Get, "/api/apply-progress") => json_resp(&get_progress().to_string()), (Method::Get, "/") => html(INDEX_HTML),+ // Pre-rendered tour narration (adom-tts, built at release time - never+ // generated on the fly; John's ruling 2026-08-24). Served from assets/.+ (Method::Get, p) if p.starts_with("/tour/step-") && p.ends_with(".mp3") => {+ let name = &p["/tour/".len()..];+ let ok_name = name.len() == 11 && name.starts_with("step-") && name[5..7].bytes().all(|b| b.is_ascii_digit());+ let bytes = if ok_name {+ dock::assets_dir().map(|d| d.join("tour").join(name)).and_then(|f| std::fs::read(f).ok())+ } else { None };+ match bytes {+ Some(b) => Response::from_data(b).with_header(hdr("audio/mpeg")),+ None => Response::from_string("no narration").with_status_code(404),+ }+ } (Method::Get, p) if p.ends_with("/favicon.svg") || p.ends_with("/favicon.ico") => { Response::from_string(FAVICON_SVG).with_header(hdr("image/svg+xml")) }