← Commit history
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"+  } }\ No newline at end of file
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"+