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.9.0: 3D viewer ships in assets/ not the binary; env map vendored for real; KiCad paths resolved at runtime
4 files changed
+152−21
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.8.0"+version = "2.9.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."
VERSION+1−1@@ -1 +1 @@-2.8.0+2.9.0
build.sh+13−6@@ -46,14 +46,21 @@ mkdir -p bin && cp target/release/adom-lbr bin/adom-lbr && chmod +x bin/adom-lbr REPORTED="$(./bin/adom-lbr --version | awk '{print $2}')" [ "$REPORTED" = "$V" ] || { echo "[build] FAIL binary reports $REPORTED, expected $V" >&2; exit 1; } -# The Babylon viewer is compiled in through include_str!, so it never shows up in-# a source diff. A binary that lost it still builds and still runs, it just serves-# a dead 3D pane, which is how three releases shipped before anyone noticed.+# The 3D viewer ships as FILES now, not inside the binary. Two ways that breaks+# quietly, so check both: the assets go missing (viewer generation dies at+# runtime), or they creep back into the binary (the 12 MB payload returns to every+# generate user, invisible in a source diff, which is how three releases shipped a+# bundle no committed source referenced).+[ -s assets/adom-3d-viewer.min.js ] || { echo "[build] FAIL assets/adom-3d-viewer.min.js missing or empty" >&2; exit 1; }+[ -s assets/environmentSpecular.env ] || { echo "[build] FAIL assets/environmentSpecular.env missing or empty" >&2; exit 1; } HITS="$(grep -a -c BABYLON bin/adom-lbr || true)"-[ "${HITS:-0}" -gt 100 ] || { echo "[build] FAIL vendored 3D viewer missing (BABYLON hits=$HITS)" >&2; exit 1; }+[ "${HITS:-0}" -lt 10 ] || { echo "[build] FAIL the viewer is back INSIDE the binary (BABYLON hits=$HITS); it should ship in assets/" >&2; exit 1; }+grep -q 'assets/\*\*' package.json || { echo "[build] FAIL package.json files[] does not ship assets/**" >&2; exit 1; } -printf '[build] OK %s %.2f MB BABYLON=%s\n' "$V" \- "$(stat -c%s bin/adom-lbr | awk '{print $1/1048576}')" "$HITS"+printf '[build] OK %s binary %.2f MB + assets %.2f MB (was one %.2f MB binary)\n' "$V" \+ "$(stat -c%s bin/adom-lbr | awk '{print $1/1048576}')" \+ "$(du -sb assets | awk '{print $1/1048576}')" \+ "$(echo "$(stat -c%s bin/adom-lbr) $(du -sb assets | cut -f1)" | awk '{print ($1+$2)/1048576}')" cat <<HINT [build] next, and do BOTH or the page stops matching the tarball:
src/manage.rs+137−13@@ -18,8 +18,14 @@ use std::sync::Mutex; use tiny_http::{Header, Method, Response, Server}; const WIKI: &str = "https://wiki.adom.inc";-// TODO: detect the signed-in wiki user rather than hard-coding this owner.-const MY_OWNER: &str = "barrett-land";+// Whose components the Manager highlights as "mine". Hard-coding one developer's+// handle showed everyone else somebody else's library as their own.+fn my_owner() -> String {+ std::env::var("ADOM_LBR_OWNER")+ .ok()+ .filter(|v| !v.trim().is_empty())+ .unwrap_or_default()+} /// The wiki component list is slow (~8s) but stable — fetch it once, serve /// instantly after. First click blocks; the rest are snappy.@@ -160,7 +166,7 @@ fn list_components() -> Value { items.push(json!({"slug": s, "title": up(s), "brief": ""})); } }- json!({ "status": "ok", "components": items, "me": MY_OWNER,+ json!({ "status": "ok", "components": items, "me": my_owner(), "hints": ["click a component to open its viewer"] }) } @@ -572,10 +578,41 @@ fn empty_svg(msg: &str) -> String { // delivered to their machine via the Adom Desktop bridge. // --------------------------------------------------------------------------- -// TODO: detect from the bridge rather than hard-coding this user's home.-const KICAD_SYM_DIR: &str = "/home/barrett-land/Documents/KiCAD/Symbols";-const KICAD_FP_DIR: &str = "/home/barrett-land/Documents/KiCAD/Footprints";-const KICAD_PROJ_DIR: &str = "/home/barrett-land/Documents/KiCAD/Projects";+// Where "Add to Library" delivers, on the USER's machine (across the desktop+// bridge), so it cannot be resolved from this process's own $HOME. These were one+// developer's absolute paths, which silently delivered nowhere for everybody else.+//+// Order: an explicit env override, then the bridge's reported home, then the+// conventional Documents/KiCAD layout under whatever home we can learn.+fn kicad_root() -> String {+ if let Ok(d) = std::env::var("ADOM_LBR_KICAD_DIR") {+ return d.trim_end_matches('/').to_string();+ }+ // The bridge knows whose machine it is; ask it before guessing.+ if let Some(home) = bridge_home() {+ return format!("{}/Documents/KiCAD", home.trim_end_matches('/'));+ }+ let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());+ format!("{}/Documents/KiCAD", home.trim_end_matches('/'))+}++/// The home directory the desktop bridge reports, when a bridge is reachable.+fn bridge_home() -> Option<String> {+ let out = Command::new("adom-desktop").args(["desktop_info"]).output().ok()?;+ if !out.status.success() {+ return None;+ }+ let v: serde_json::Value = serde_json::from_slice(&out.stdout).ok()?;+ v.get("home")+ .or_else(|| v.get("home_dir"))+ .or_else(|| v.pointer("/result/home"))+ .and_then(|h| h.as_str())+ .map(|s| s.to_string())+}++fn kicad_sym_dir() -> String { format!("{}/Symbols", kicad_root()) }+fn kicad_fp_dir() -> String { format!("{}/Footprints", kicad_root()) }+fn kicad_proj_dir() -> String { format!("{}/Projects", kicad_root()) } const SCRATCH: &str = "/tmp/adom-lbr-apply"; const PYTHON_SELFCONTAIN: &str = include_str!("selfcontain.py");@@ -586,7 +623,7 @@ const PYTHON_SELFCONTAIN: &str = include_str!("selfcontain.py"); /// library, re-point every `lib_id`/footprint reference to it, and write the /// project-local sym/fp-lib-tables. Then deliver everything to `dest`. fn fetch_project_files(board: &str, dest: &str, project_only: bool) -> (String, Vec<String>) {- let dest = if dest.trim().is_empty() { format!("{KICAD_PROJ_DIR}/{board}") } else { dest.trim().to_string() };+ let dest = if dest.trim().is_empty() { format!("{}/{board}", kicad_proj_dir()) } else { dest.trim().to_string() }; let work = format!("{SCRATCH}/proj-{}", board.replace(['/', ' '], "_")); let _ = std::fs::remove_dir_all(&work); let _ = std::fs::create_dir_all(&work);@@ -896,7 +933,7 @@ fn run_apply(items: Vec<(String, String, String)>, board: Option<String>, dest: } } prog(format!("Merging {} symbol(s) into {symlib}", blocks.len()), done_n);- let remote = format!("{KICAD_SYM_DIR}/{symlib}.kicad_sym");+ let remote = format!("{}/{symlib}.kicad_sym", kicad_sym_dir()); let existing = ad_pull(&remote).map(|b| String::from_utf8_lossy(&b).to_string()); // NEVER overwrite a library we failed to read. ad_pull returns None for // both "absent" and "pull failed" — only proceed with a fresh library@@ -933,7 +970,7 @@ fn run_apply(items: Vec<(String, String, String)>, board: Option<String>, dest: errors.push(format!("failed to stage {symlib}.kicad_sym locally: {e}")); continue; }- if ad_send(&[local], KICAD_SYM_DIR) {+ if ad_send(&[local], &kicad_sym_dir()) { for (name, _) in &blocks { added.push(json!({"symbol": name, "symLib": symlib})); }@@ -944,7 +981,7 @@ fn run_apply(items: Vec<(String, String, String)>, board: Option<String>, dest: for (fplib, slugs) in &by_fplib { prog(format!("Delivering footprints → {fplib}.pretty"), total);- let dest = format!("{KICAD_FP_DIR}/{fplib}.pretty");+ let dest = format!("{}/{fplib}.pretty", kicad_fp_dir()); ad_shell(&format!("mkdir -p {}", sh_quote(&dest))); let mut files = vec![]; for slug in slugs {@@ -999,6 +1036,87 @@ const INDEX_HTML: &str = include_str!("manage.html"); // 3-column embed. Symbol/footprint SVG are rendered on the fly from the hub; // pin type + datasheet description come from <MPN>-symbol.extracted.json. // ---------------------------------------------------------------------------+// ── Viewer assets ──────────────────────────────────────────────────────────+//+// The Babylon bundle is 12 MB. It used to ride inside the binary through+// include_str!, which meant every `adom-lbr generate` user carried it for a+// `manage` subcommand they may never open, and, worse, made the payload+// invisible in a source diff: three releases shipped a bundle that no committed+// source referenced before anyone noticed. It ships as a file now, so it is the+// same bytes on disk and visible in every diff.++/// Where the viewer assets live at runtime, in the order worth trying.+fn asset_dir() -> Option<std::path::PathBuf> {+ let mut cands: Vec<std::path::PathBuf> = Vec::new();+ if let Ok(d) = std::env::var("ADOM_LBR_ASSETS") {+ cands.push(std::path::PathBuf::from(d));+ }+ if let Ok(exe) = std::env::current_exe() {+ if let Some(dir) = exe.parent() {+ cands.push(dir.join("assets")); // portable, beside the binary+ cands.push(dir.join("../share/adom-lbr")); // a prefix install+ }+ }+ if let Ok(home) = std::env::var("HOME") {+ cands.push(std::path::PathBuf::from(home).join(".local/share/adom-lbr"));+ }+ cands.push(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets")); // dev tree+ cands.into_iter().find(|d| d.join("adom-3d-viewer.min.js").is_file())+}++/// The 3D bundle, ready to inline into a generated viewer.+///+/// Escapes any literal `</script>` so the bundle cannot close our tag early. The+/// blob store serves .js as application/octet-stream + nosniff, which blocks+/// `<script src>`, so inlining is not a stylistic choice.+fn viewer_bundle() -> Result<String, String> {+ let dir = asset_dir().ok_or_else(|| {+ "3D viewer assets not found. Looked at $ADOM_LBR_ASSETS, ./assets beside the binary, \+../share/adom-lbr, and ~/.local/share/adom-lbr. Reinstall adom-lbr, or point ADOM_LBR_ASSETS \+at the directory holding adom-3d-viewer.min.js.".to_string()+ })?;+ let p = dir.join("adom-3d-viewer.min.js");+ std::fs::read_to_string(&p)+ .map(|b| b.replace("</script", "<\\/script"))+ .map_err(|e| format!("cannot read {}: {}", p.display(), e))+}++/// The environment map, as a data: URI.+///+/// The bundle ships a hardcoded CDN URL for this texture, so vendoring the file+/// was only half the job: until it is substituted in, a generated viewer still+/// reaches out to assets.babylonjs.com and does not render offline. Returns None+/// when the file is absent, and the viewer falls back to the CDN as before.+fn environment_data_uri() -> Option<String> {+ let bytes = std::fs::read(asset_dir()?.join("environmentSpecular.env")).ok()?;+ Some(format!("data:application/octet-stream;base64,{}", b64_encode(&bytes)))+}++const BABYLON_ENV_CDN: &str = "https://assets.babylonjs.com/core/environments/environmentSpecular.env";++fn b64_encode(bytes: &[u8]) -> String {+ const T: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";+ let mut out = String::with_capacity((bytes.len() + 2) / 3 * 4);+ for c in bytes.chunks(3) {+ let b = [c[0], *c.get(1).unwrap_or(&0), *c.get(2).unwrap_or(&0)];+ let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;+ out.push(T[(n >> 18 & 63) as usize] as char);+ out.push(T[(n >> 12 & 63) as usize] as char);+ out.push(if c.len() > 1 { T[(n >> 6 & 63) as usize] as char } else { '=' });+ out.push(if c.len() > 2 { T[(n & 63) as usize] as char } else { '=' });+ }+ out+}++/// The bundle with its CDN environment-texture URL swapped for the vendored file.+fn viewer_bundle_offline() -> Result<String, String> {+ let b = viewer_bundle()?;+ Ok(match environment_data_uri() {+ Some(uri) => b.replace(BABYLON_ENV_CDN, &uri),+ None => b,+ })+}+ pub fn export_viewers(slug: &str, dir: &std::path::Path, out_dir: &std::path::Path, no_pin_table: bool) -> Result<(), String> { let mpn = up(slug); // Prefer the canonical local hub (<MPN>.adom-lbr.json in --dir); fall back to@@ -1074,7 +1192,10 @@ pub fn export_viewers(slug: &str, dir: &std::path::Path, out_dir: &std::path::Pa // application/octet-stream + nosniff, which blocks <script src>; inline // scripts execute under script-src 'unsafe-inline'. Escape any literal // </script> in the bundle so it can't close our tag early.- let bundle = include_str!("adom-3d-viewer.min.js").replace("</script", "<\\/script");+ let bundle = viewer_bundle_offline().unwrap_or_else(|e| {+ eprintln!("ERROR: {e}");+ std::process::exit(1);+ }); // laser marking lines: the MPN (chips are laser-marked; the STEP never has it) let marks_json = serde_json::to_string(&vec![mpn.clone()]).unwrap_or_else(|_| "[]".into()); let three = include_str!("viewer3d.html")@@ -1176,7 +1297,10 @@ pub fn export_composed( // inline the Babylon bundle only when a 3D pane is present let bundle = if panes.iter().any(|p| p == "3d") {- include_str!("adom-3d-viewer.min.js").replace("</script", "<\\/script")+ viewer_bundle_offline().unwrap_or_else(|e| {+ eprintln!("ERROR: {e}");+ std::process::exit(1);+ }) } else { String::new() };