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.0: interactive 3D embed (inline model-viewer + data-URI GLB, Draco transcoded)
8 files changed
+143−17
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-lbr"-version = "2.11.10"+version = "2.12.0" dependencies = [ "altium-codec", "anyhow",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.11.10"+version = "2.12.0" 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."
README.md+7@@ -67,6 +67,13 @@ skill documents every piece of it for your AI. ## The 3-column library viewer, with an interactive embed +The embed's 3D column is a REAL interactive viewer, not a picture: when the+part's directory holds a `.glb`, the model rides inline (Draco models are+transcoded to quantized at build time, measured ~35% smaller) with the+model-viewer runtime inlined beside it, so the finished readme.html renders an+orbitable 3D chip even inside the wiki's no-network readme sandbox. No `.glb`+means the static render, unchanged.+  ▶ **[Open the live ADS1115 3-column viewer](https://wiki.adom.inc/api/v1/pages/john/adom-lbr/files/demo/ads1115-viewer.html)**: the exact self-contained file `adom-lbr embed` produces, served straight from this page (pan/zoom each panel; the pin→pad table is fully populated from the datasheet). It is built to sit in an `<iframe>` once the wiki enables component-page iframe embedding.
VERSION+1−1@@ -1 +1 @@-2.11.10+2.12.0
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-lbr",- "version": "2.11.10",+ "version": "2.12.0", "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.11.10",+ "version": "2.12.0", "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" } ]-}+}
src/embed.html+3@@ -53,6 +53,9 @@ var PINS=__PINS__; (function(){ // each viewport: pan/zoom on its <svg> or <img> document.querySelectorAll('[data-vp]').forEach(function(vp){+ // The 3D pane is a real viewer with its own orbit/zoom - panning a+ // projection was the bug (adom-lbr#4), so it opts out of this loop.+ if(vp.querySelector('model-viewer')) return; var el=vp.querySelector('svg,img'); if(!el) return; if(el.tagName.toLowerCase()==='svg'){ el.removeAttribute('width'); el.removeAttribute('height'); } var s=1,tx=0,ty=0;
src/main.rs+120−1@@ -2323,6 +2323,111 @@ fn read_first(dir: &std::path::Path, names: &[String]) -> Option<String> { None } +/// Find the directory's model, transcode Draco away when present, and emit a+/// self-contained interactive 3D pane (inlined model-viewer + data-URI GLB ++/// the static iso render as the poster). Err(reason) falls back to the PNG.+fn build_3d_viewer(dir: &std::path::Path, mpn: &str) -> Result<String, String> {+ // prefer <MPN>.glb (any case), else the first .glb in the directory+ let mut glb_path: Option<PathBuf> = None;+ if let Ok(rd) = fs::read_dir(dir) {+ let mut all: Vec<PathBuf> = rd.flatten().map(|e| e.path())+ .filter(|p| p.extension().and_then(|e| e.to_str()).map(|e| e.eq_ignore_ascii_case("glb")).unwrap_or(false))+ .collect();+ all.sort();+ glb_path = all.iter().find(|p| {+ p.file_stem().and_then(|s| s.to_str()).map(|s| s.eq_ignore_ascii_case(mpn)).unwrap_or(false)+ }).cloned().or_else(|| all.first().cloned());+ }+ let glb_path = glb_path.ok_or("no .glb in this directory")?;+ let mut bytes = fs::read(&glb_path).map_err(|e| format!("read {}: {e}", glb_path.display()))?;+ let orig_len = bytes.len();++ if glb_is_draco(&bytes) {+ let tool = ensure_gltf_transform()?;+ let out_tmp = std::env::temp_dir().join(format!("adom-lbr-{mpn}-quantized.glb"));+ let st = process::Command::new(&tool)+ .args(["quantize", glb_path.to_str().unwrap_or_default(), out_tmp.to_str().unwrap_or_default()])+ .output()+ .map_err(|e| format!("gltf-transform did not run: {e}"))?;+ if !st.status.success() {+ return Err(format!("gltf-transform quantize failed: {}", String::from_utf8_lossy(&st.stderr).lines().last().unwrap_or("")));+ }+ bytes = fs::read(&out_tmp).map_err(|e| format!("read transcode output: {e}"))?;+ if glb_is_draco(&bytes) {+ return Err("transcode output still requires the Draco decoder".into());+ }+ println!("HINT: 3D model transcoded Draco -> quantized for the sandbox: {} -> {} bytes", orig_len, bytes.len());+ }++ let viewer_js = ensure_model_viewer()?;+ let poster = ["-3d-iso-named.png", "-3d-iso.png", "-3d-iso-lg.png"]+ .iter().map(|s| dir.join(format!("{mpn}{s}"))).find(|p| p.exists())+ .and_then(|p| fs::read(&p).ok())+ .map(|b| format!(" poster=\"data:image/png;base64,{}\"", b64(&b)))+ .unwrap_or_default();+ Ok(format!(+ "<script type=\"module\">{viewer_js}</script>\n\+ <model-viewer src=\"data:model/gltf-binary;base64,{model}\"{poster} camera-controls auto-rotate shadow-intensity=\"1\" style=\"position:absolute;inset:0;width:100%;height:100%;background:#161c27\"></model-viewer>",+ model = b64(&bytes)+ ))+}++/// GLB magic + JSON chunk scan for the Draco extension requirement.+fn glb_is_draco(bytes: &[u8]) -> bool {+ if bytes.len() < 20 || &bytes[0..4] != b"glTF" {+ return false;+ }+ let json_len = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]) as usize;+ let end = (20 + json_len).min(bytes.len());+ let json = String::from_utf8_lossy(&bytes[20..end]);+ json.contains("KHR_draco_mesh_compression")+}++/// The gltf-transform CLI, installed once into the adom-lbr cache.+fn ensure_gltf_transform() -> Result<PathBuf, String> {+ let home = std::env::var("HOME").map_err(|_| "no HOME")?;+ let prefix = std::path::Path::new(&home).join(".cache/adom-lbr/gltf");+ let bin = prefix.join("node_modules/.bin/gltf-transform");+ if bin.exists() {+ return Ok(bin);+ }+ process::Command::new("node").arg("-v").output().map_err(|_| "node is not installed (needed once to transcode a Draco .glb)")?;+ println!("HINT: fetching @gltf-transform/cli into {} (once)…", prefix.display());+ let _ = fs::create_dir_all(&prefix);+ let st = process::Command::new("npm")+ .args(["install", "--prefix", prefix.to_str().unwrap_or_default(), "--no-fund", "--no-audit", "@gltf-transform/cli@3", "draco3dgltf"])+ .output()+ .map_err(|e| format!("npm did not run: {e}"))?;+ if !st.status.success() || !bin.exists() {+ return Err(format!("npm install @gltf-transform/cli@3 failed (v3 pinned: v4 needs Node 20+): {}", String::from_utf8_lossy(&st.stderr).lines().last().unwrap_or("")));+ }+ Ok(bin)+}++/// The model-viewer bundle, fetched once into the cache and inlined thereafter+/// (the readme sandbox may fetch nothing at view time, so it must ride along).+fn ensure_model_viewer() -> Result<String, String> {+ let home = std::env::var("HOME").map_err(|_| "no HOME")?;+ let path = std::path::Path::new(&home).join(".cache/adom-lbr/model-viewer-3.5.0.min.js");+ if !path.exists() {+ let _ = fs::create_dir_all(path.parent().unwrap());+ let st = process::Command::new("curl")+ .args(["-sL", "--max-time", "60", "-o", path.to_str().unwrap_or_default(),+ "https://unpkg.com/@google/[email protected]/dist/model-viewer.min.js"])+ .status()+ .map_err(|e| format!("curl did not run: {e}"))?;+ if !st.success() {+ return Err("could not fetch the model-viewer bundle (offline?); the static render stands in".into());+ }+ }+ let js = fs::read_to_string(&path).map_err(|e| format!("read model-viewer bundle: {e}"))?;+ if js.len() < 100_000 {+ let _ = fs::remove_file(&path);+ return Err("model-viewer bundle looks truncated; refetch next run".into());+ }+ Ok(js)+}+ fn cmd_embed(dir: &std::path::Path, out: Option<PathBuf>) -> Result<(), String> { let mpn = dir.file_name().and_then(|s| s.to_str()).ok_or("bad --dir")?.to_string(); // renders@@ -2331,7 +2436,14 @@ fn cmd_embed(dir: &std::path::Path, out: Option<PathBuf>) -> Result<(), String> .unwrap_or_else(|| "<div class=\"ph\">no symbol render in this directory</div>".into()); let fp_svg = read_first(dir, &[format!("{mpn}-footprint.authoritative.svg"), format!("{mpn}-footprint.svg")]) .unwrap_or_else(|| "<div class=\"ph\">no footprint render in this directory</div>".into());- let threed = {+ // The 3D column: a real interactive viewer when the directory has a .glb+ // (adom-lbr#4, barrett's design carried forward). The model rides INLINE as+ // a data URI and the model-viewer bundle is inlined too, because the wiki+ // serves readmes with connect-src 'none': nothing may be fetched at view+ // time. Draco models are transcoded to KHR_mesh_quantization at build time+ // (three.js decodes that natively; the Draco decoder is a CDN fetch the+ // sandbox blocks - and quantized is measured ~40% smaller anyway).+ let static_3d = || { let cands = ["-3d-iso-named.png", "-3d-iso.png", "-3d-iso-lg.png"] .iter().map(|s| dir.join(format!("{mpn}{s}"))).find(|p| p.exists()); match cands.and_then(|p| fs::read(&p).ok()) {@@ -2339,6 +2451,13 @@ fn cmd_embed(dir: &std::path::Path, out: Option<PathBuf>) -> Result<(), String> None => "<div class=\"ph\">no 3D render in this directory</div>".into(), } };+ let threed = match build_3d_viewer(dir, &mpn) {+ Ok(html) => html,+ Err(reason) => {+ eprintln!("HINT: 3D column falls back to the static render: {reason}");+ static_3d()+ }+ }; // optional pin→pad connects from a .lbr let mut pin_to_pad: std::collections::HashMap<String, String> = std::collections::HashMap::new(); if let Some(lbr) = read_first(dir, &[format!("{mpn}.lbr")]) {