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.4
10 files changed
+183−41
docs/var-groups-grouped.pngadded⋯ 1 unchanged line ⋯
docs/var-name-insymbol.pngadded⋯ 1 unchanged line ⋯
install.sh+12−2@@ -1,4 +1,14 @@ #!/bin/bash+# install.sh for adom-symbol (app).+# Runs from the extracted module directory (~/project/adom_modules/adom-symbol/). set -euo pipefail-DIR="$(cd "$(dirname "$0")" && pwd)"; BIN="${HOME}/.local/bin"; mkdir -p "$BIN"-install -m 0755 "$DIR/bin/adom-symbol" "$BIN/adom-symbol"; echo "Installed adom-symbol"++DIR="$(cd "$(dirname "$0")" && pwd)"+cd "$DIR"++# App install: link the binary onto PATH.+BIN_DIR="${HOME}/.local/bin"+mkdir -p "$BIN_DIR"+install -m 0755 "$DIR/bin/adom-symbol" "$BIN_DIR/adom-symbol"+echo "Installed adom-symbol -> $BIN_DIR/adom-symbol"+
package.json+37−9@@ -1,8 +1,8 @@ { "slug": "adom-symbol",- "version": "1.0.3",+ "version": "1.0.4", "type": "app",- "description": "Lay out the pins, emboss the chip 3D model, and export an adom-UI interactive embed for component pages.",+ "description": "A live, AI-drivable KiCad schematic-symbol studio \u2014 auto-grouped pins, the chip's real 3D model embossed on the symbol, sent to KiCad/Fusion/Altium/OrCAD.", "dependencies": {}, "tags": [ "eda",@@ -12,13 +12,41 @@ "pcb", "cli" ],- "hero": {- "type": "billboard",- "headline": "Symbols, studio-grade.",- "subhead": "Lay out the pins, emboss the chip 3D model, and export an adom-UI interactive embed for component pages.",- "screenshot": "screenshots/shot.png",- "accent": "#00b8b1"- },+ "files": [+ "install.sh",+ "uninstall.sh",+ "package.json",+ "page.json",+ "README.md",+ "SKILL.md",+ "Cargo.toml",+ "screenshots/hero-v5.png",+ "src/main.rs",+ "src/app.html",+ "src/render.rs",+ "src/discrete_gen.rs",+ "src/embed.html",+ "src/deliver.rs",+ "src/server.rs",+ "src/theme.rs",+ "src/ds2sf.rs",+ "src/parse.rs",+ "src/sym_gen.rs",+ "docs/var-name-onchip.png",+ "docs/shot.png",+ "docs/var-name-insymbol.png",+ "docs/var-3d-outline.png",+ "docs/var-3d-iso.png",+ "docs/var-groups-grouped.png",+ "docs/var-groups-flat.png",+ "docs/var-layout-square.png",+ "docs/symbol.png",+ "docs/embed.png",+ "docs/hero.png",+ "docs/var-layout-leftright.png",+ "docs/chip3d.png",+ "docs/studio.png"+ ], "scripts": { "install": "./install.sh", "uninstall": "./uninstall.sh"
page.json+38−11@@ -1,8 +1,8 @@ { "slug": "adom-symbol",- "version": "1.0.3",+ "version": "1.0.4", "type": "app",- "description": "Lay out the pins, emboss the chip 3D model, and export an adom-UI interactive embed for component pages.",+ "description": "A live, AI-drivable KiCad schematic-symbol studio — auto-grouped pins, the chip's real 3D model embossed on the symbol, sent to KiCad/Fusion/Altium/OrCAD.", "dependencies": {}, "tags": [ "eda",@@ -12,13 +12,41 @@ "pcb", "cli" ],- "hero": {- "type": "billboard",- "headline": "Symbols, studio-grade.",- "subhead": "Auto-grouped pins, the chip's real 3D model embossed on the symbol, sent to KiCad, Fusion, Altium or OrCAD.",- "screenshot": "docs/shot.png",- "accent": "#00b8b1"- },+ "files": [+ "install.sh",+ "uninstall.sh",+ "package.json",+ "page.json",+ "README.md",+ "SKILL.md",+ "Cargo.toml",+ "screenshots/hero-v5.png",+ "src/main.rs",+ "src/app.html",+ "src/render.rs",+ "src/discrete_gen.rs",+ "src/embed.html",+ "src/deliver.rs",+ "src/server.rs",+ "src/theme.rs",+ "src/ds2sf.rs",+ "src/parse.rs",+ "src/sym_gen.rs",+ "docs/var-name-onchip.png",+ "docs/shot.png",+ "docs/var-name-insymbol.png",+ "docs/var-3d-outline.png",+ "docs/var-3d-iso.png",+ "docs/var-groups-grouped.png",+ "docs/var-groups-flat.png",+ "docs/var-layout-square.png",+ "docs/symbol.png",+ "docs/embed.png",+ "docs/hero.png",+ "docs/var-layout-leftright.png",+ "docs/chip3d.png",+ "docs/studio.png"+ ], "scripts": { "install": "./install.sh", "uninstall": "./uninstall.sh"@@ -28,6 +56,5 @@ "author": { "name": "John Lauer", "email": "[email protected]"- },- "title": "Adom Symbol"+ } }
screenshots/hero-v5.pngadded⋯ 1 unchanged line ⋯
src/app.html+42−15@@ -398,7 +398,7 @@ const bb=computeBounds(); if(bb){const r=document.createElementNS(NS,'rect');r.id='bbox';r.setAttribute('x',bb.x);r.setAttribute('y',bb.y);r.setAttribute('width',bb.w);r.setAttribute('height',bb.h);svgEl.appendChild(r)} const pm=document.createElementNS(NS,'circle');pm.id='pinmark';svgEl.appendChild(pm);- const HT=1.05; // half-thickness across the wire (< 2.54 pitch)+ const HT=1.27; // half-thickness across the wire (~pitch*0.95, like the original) const bw=window._body?Math.abs(window._body.x2-window._body.x1):14; const bh=window._body?Math.abs(window._body.y2-window._body.y1):14; pins.forEach((p,idx)=>{@@ -407,7 +407,9 @@ let dx=0,dy=0; if(p.angle===0)dx=1; else if(p.angle===180)dx=-1; else if(p.angle===90)dy=-1; else if(p.angle===270)dy=1; const horiz=dx!==0;- const nameDepth=Math.min(7,(horiz?bw:bh)*0.42);+ // Reach all the way to the body centre so the zone covers the WHOLE pin+ // name (long names like P0.04/GRTC_CLKOUT32K), not just the first 7mm.+ const nameDepth=(horiz?bw:bh)*0.5; const reach=L+nameDepth; // wire + into-body name region const outX=tipX-dx*0.6, outY=tipY-dy*0.6; // a touch beyond the tip (number) const inX=tipX+dx*reach, inY=tipY+dy*reach;@@ -507,7 +509,12 @@ const SRC_LABEL={auto:'Auto · grouped',ds2sf:'ds2sf',manufacturer:'Manufacturer', snapeda:'SnapEDA',snapmagic:'SnapEDA',ultralibrarian:'Ultra Librarian',cse_ul:'Ultra Librarian', componentsearch:'Component Search',mouser:'Mouser',digikey:'DigiKey',arrow:'Arrow',lcsc:'LCSC',vendor:'Vendor'};- document.getElementById('studioBtn').onclick=()=>{studio.hidden=!studio.hidden;document.getElementById('studioBtn').classList.toggle('active',!studio.hidden)};+ function setStudioOpen(open){studio.hidden=!open;document.getElementById('studioBtn').classList.toggle('active',open);try{localStorage.setItem('adomSymStudioOpen',open?'1':'0')}catch(e){}}+ document.getElementById('studioBtn').onclick=()=>setStudioOpen(studio.hidden);+ // On a wide laptop/desktop the Layout studio floats OPEN by default (luxurious+ // screen real-estate); on a phone it stays closed. An explicit open/close sticks.+ (function(){var pref=null;try{pref=localStorage.getItem('adomSymStudioOpen')}catch(e){}+ setStudioOpen(pref===null ? window.innerWidth>=1024 : pref==='1');})(); // ── floaty studio on wide screens: drag / collapse / close + remembered pos ── (function(){ const wide=()=>matchMedia('(min-width:821px)').matches;@@ -515,7 +522,7 @@ const FK='adomSymbolStudioHud'; let st={}; try{st=JSON.parse(localStorage.getItem(FK))||{}}catch{} function applyPos(){ if(!wide())return; if(st.x!=null){studio.style.right='auto';studio.style.left=st.x+'px';studio.style.top=st.y+'px'} }- document.getElementById('studioClose').onclick=()=>{studio.hidden=true;document.getElementById('studioBtn').classList.remove('active')};+ document.getElementById('studioClose').onclick=()=>setStudioOpen(false); document.getElementById('studioCollapse').onclick=()=>{const c=studio.classList.toggle('collapsed');document.getElementById('studioCollapse').textContent=c?'▸':'▾'}; head.addEventListener('dblclick',e=>{if(!e.target.closest('.shbtn'))document.getElementById('studioCollapse').click()}); let drag=false,dx=0,dy=0;@@ -525,7 +532,7 @@ applyPos(); })(); // sticky studio preferences (per browser)- const PREF_KEY='adomSymbolStudio2'; // bumped: clears stale 3D="bold" sticky so the iso default shows+ const PREF_KEY='adomSymbolStudio3'; // bumped: new defaults are bold 3D chip + on-chip laser name function loadPrefs(){try{return JSON.parse(localStorage.getItem(PREF_KEY)||'{}')||{}}catch{return {}}} function savePrefs(p){try{localStorage.setItem(PREF_KEY,JSON.stringify(p))}catch(e){}} let relaying=false, prefsAppliedFor=null;@@ -555,7 +562,8 @@ const s=window._lastState||{}; const src=(s.sources||[]).includes('auto')?'auto':((s.sources||[])[0]||'manufacturer'); prefsAppliedFor=s.symbolName||'__reset__'; // don't re-apply old prefs- relayout({source:src,pinLayout:'left-right',grouped:true,refPos:'out-top',valuePos:'out-bot',threeD:'iso'},false);+ nameMode='chip';+ relayout({source:src,pinLayout:'left-right',grouped:true,refPos:'out-top',valuePos:'none',threeD:'bold',laserName:true},false); }; function seg(id,onClick){document.getElementById(id).addEventListener('click',e=>{const b=e.target.closest('button[data-v]');if(b)onClick(b.dataset.v)})} seg('segSource',v=>relayout({source:v}));@@ -570,7 +578,7 @@ seg('segValSide',v=>{valSide=v;if(nameMode==='symbol'||nameMode==='both')relayout({valuePos:posOf(valSide,valIO)})}); seg('segValIO',v=>{valIO=v;if(nameMode==='symbol'||nameMode==='both')relayout({valuePos:posOf(valSide,valIO)})}); // Chip-name display mode: maps to the schematic Value position + laser-on-chip flag.- let nameMode='symbol';+ let nameMode='chip'; seg('segName',v=>{ nameMode=v; const showSym=(v==='symbol'||v==='both'); relayout({ valuePos: showSym?posOf(valSide,valIO):'none', laserName: (v==='chip'||v==='both') });@@ -659,18 +667,13 @@ // not just big square ones. ~72% of the smaller body dimension. const sz=Math.min(w,h)*(mode==='iso'?0.72:0.74); const NS='http://www.w3.org/2000/svg';- // Contrast disc behind the (often dark) iso package so it reads on the dark- // body fill. Subtle; skipped for the line-art outline modes.- if(mode==='iso'){- const bg=document.createElementNS(NS,'ellipse');bg.id='iso3d-bg';- bg.setAttribute('cx',cx);bg.setAttribute('cy',cy);bg.setAttribute('rx',sz*0.62);bg.setAttribute('ry',sz*0.52);- bg.setAttribute('fill','rgba(150,170,200,0.14)');bg.style.pointerEvents='none';- svgEl.appendChild(bg);- } const img=document.createElementNS(NS,'image');img.id='iso3d'; img.setAttribute('x',cx-sz/2);img.setAttribute('y',cy-sz/2);img.setAttribute('width',sz);img.setAttribute('height',sz); img.setAttribute('href',asset);img.setAttributeNS('http://www.w3.org/1999/xlink','href',asset);img.setAttribute('preserveAspectRatio','xMidYMid meet'); img.setAttribute('opacity',mode==='iso'?'0.95':'1');img.style.pointerEvents='none';+ // thin/bold are dark-stroke line art (made for light/print backgrounds) — invert+ // them so they read as crisp WHITE line art on the dark symbol body.+ if(mode==='thin'||mode==='bold'){ img.style.filter='invert(1) brightness(1.15)'; } // Layering: body fill paints in a later group (it occludes anything below), // so we append the chip ON TOP of the body, then lift every text/label group // back above the chip → final order is body → chip → text (labels stay pure@@ -690,6 +693,30 @@ // ships to KiCad. The <image> above already shows it. } + // ── AI-driving API (ai-driven-apps contract): every user action has a named+ // command that drives the real UI the same way a click does. The CLI calls+ // these over the /eval channel; a person clicking produces identical results.+ window.ui = {+ studio(open){ setStudioOpen(open!==false&&open!=='close'); return 'studio '+(studio.hidden?'closed':'open'); },+ set(key,val){+ if(key==='source') relayout({source:val});+ else if(key==='layout') relayout({pinLayout:val});+ else if(key==='groups') relayout({grouped:val==='grouped'||val==='1'||val===true});+ else if(key==='3d'||key==='threed') relayout({threeD:val});+ else if(key==='name'){ nameMode=val; const showSym=(val==='symbol'||val==='both'); relayout({valuePos:showSym?posOf(valSide,valIO):'none', laserName:(val==='chip'||val==='both')}); }+ else if(key==='ref') relayout({refPos:val});+ else return 'unknown key: '+key;+ return key+'='+val;+ },+ zoom(dir){ if(dir==='in')document.getElementById('zin').click(); else if(dir==='out')document.getElementById('zout').click(); else fit(); return 'zoom '+dir; },+ send(target){ const b=document.querySelector('#segSend button[data-v="'+target+'"]'); if(b){ b.click(); return 'send to '+target; } return 'no send target: '+target; },+ hover(num){ const p=(pins||[]).find(x=>String(x.number)===String(num)); if(p){ showPin(p, innerWidth/2, 200, true); mark(p); return 'pin '+num+' = '+p.name+' ('+p.type+')'; } return 'no pin '+num; },+ toast(msg){ showToast(String(msg)); return 'toast: '+msg; },+ click(id){ const e=document.getElementById(id); if(e){ e.click(); return 'clicked #'+id; } return 'no element #'+id; },+ state(){ return window._lastState||{}; },+ list(){ return Object.keys(window.ui).filter(k=>typeof window.ui[k]==='function'); }+ };+ // ── eval channel ── async function evalLoop(){ try{const r=await fetch('eval/pending',{cache:'no-store'});const j=await r.json();
src/main.rs+46@@ -79,6 +79,19 @@ enum Cmd { #[arg(long)] sym: Option<PathBuf>, },+ /// Drive the RUNNING live app's UI from the CLI (the ai-driven-apps contract:+ /// every user action has a command that drives the real UI like a click).+ /// Examples: `ui set 3d bold` · `ui set name chip` · `ui studio open` ·+ /// `ui zoom fit` · `ui send kicad` · `ui hover 6` · `ui toast "hi"` ·+ /// `ui state` · `ui list`. Requires `serve` running + a browser tab open.+ Ui {+ /// Action: set | studio | zoom | send | hover | toast | click | state | list+ action: String,+ /// Arguments to the action (e.g. `set 3d bold` → key=3d value=bold)+ args: Vec<String>,+ #[arg(long, default_value_t = 8781)]+ port: u16,+ }, /// 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.@@ -185,6 +198,7 @@ fn run() -> Result<()> { Ok(()) } Cmd::RenderDs2sf { file, out, layout, sym } => cmd_render_ds2sf(file, out, layout, sym),+ Cmd::Ui { action, args, port } => cmd_ui(action, args, port), 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 } => {@@ -327,6 +341,38 @@ fn render_and_view(kicad_sym: &str, symbol_name: &str, folder: &std::path::Path, Ok((svg, html_path)) } +/// Drive the running app's `window.ui` API over the /eval channel.+fn cmd_ui(action: String, args: Vec<String>, port: u16) -> Result<()> {+ let arglist: Vec<String> = args.iter().map(|a| serde_json::to_string(a).unwrap_or_else(|_| format!("{a:?}"))).collect();+ let code = format!("(window.ui && window.ui.{action}) ? window.ui.{action}({}) : 'window.ui not ready — is a browser tab open?'", arglist.join(","));+ let base = format!("http://127.0.0.1:{port}");+ let client = reqwest::blocking::Client::builder().timeout(std::time::Duration::from_secs(10)).build()?;+ let resp = client+ .post(format!("{base}/eval"))+ .json(&serde_json::json!({ "code": code }))+ .send()+ .with_context(|| format!("POST {base}/eval — is the app running? (adom-symbol serve --port {port})"))?;+ let id = resp.json::<serde_json::Value>()?.get("id").and_then(|v| v.as_str()).map(String::from).ok_or_else(|| anyhow!("no eval id from server"))?;+ for _ in 0..60 {+ std::thread::sleep(std::time::Duration::from_millis(120));+ let v: serde_json::Value = client.get(format!("{base}/eval/{id}")).send()?.json().unwrap_or(serde_json::Value::Null);+ // server wraps as { done, result: { error, result } } (the front-end's payload)+ if v.get("done").and_then(|d| d.as_bool()) == Some(true) {+ let inner = v.get("result").cloned().unwrap_or(serde_json::Value::Null);+ if let Some(err) = inner.get("error") {+ if !err.is_null() { return Err(anyhow!("UI error: {}", err.as_str().unwrap_or(""))); }+ }+ match inner.get("result") {+ Some(r) if r.is_string() => println!("{}", r.as_str().unwrap_or("")),+ Some(r) if !r.is_null() => println!("{}", serde_json::to_string_pretty(r).unwrap_or_default()),+ _ => println!("ok"),+ }+ return Ok(());+ }+ }+ Err(anyhow!("timed out — open a browser tab on http://127.0.0.1:{port}/ so the UI can run the command"))+}+ 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()));
src/server.rs+3−3@@ -272,9 +272,9 @@ fn load_from_dir(dir: &str, mpn: &str, init_source: &str) { pin_layout: "left-right".to_string(), grouped: true, ref_pos: "out-top".to_string(),- value_pos: "out-bot".to_string(),- laser_name: false,- three_d: "iso".to_string(), // default: show the iso chip in the body centre+ value_pos: "none".to_string(), // name lives ON the chip by default, not in the symbol+ laser_name: true, // default: laser-mark the chip name onto the 3D package+ three_d: "bold".to_string(), // default: the BOLD vector-outline chip in the body centre }); regenerate(); }
uninstall.sh+5−1@@ -1,3 +1,7 @@ #!/bin/bash+# uninstall.sh for adom-symbol (app). set -euo pipefail-rm -f "${HOME}/.local/bin/adom-symbol"; echo "Uninstalled adom-symbol"++rm -f "${HOME}/.local/bin/adom-symbol"+echo "Uninstalled adom-symbol"+