app
Adom Footprint
Public Made by Adomby adom
KiCad footprint creator with layer HUD viewer and delivery to KiCad/Fusion 360
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
//! "Send to" delivery — push the current footprint to the user's desktop EDA
//! tools via the adom-desktop bridge. KiCad takes the native `.kicad_mod`
//! directly (footprint-only); the EAGLE-family tools (Fusion 360 / Altium /
//! OrCAD) import a true `.lbr`, which adom-lbr builds from the footprint pads.
//! Best-effort: a clear message when the bridge is offline.
use serde_json::{json, Value};
use std::process::Command;
/// Run an adom-desktop verb with a JSON payload; return parsed JSON (or wrapped
/// raw output). Errors only if adom-desktop itself can't be spawned.
fn ad(verb: &str, payload: Value) -> Result<Value, String> {
let out = Command::new("adom-desktop")
.args([verb, &payload.to_string()])
.output()
.map_err(|e| format!("adom-desktop unreachable: {e}"))?;
let txt = String::from_utf8_lossy(&out.stdout).to_string();
Ok(serde_json::from_str::<Value>(&txt).unwrap_or_else(|_| json!({ "ok": out.status.success(), "raw": txt })))
}
fn looks_offline(v: &Value) -> bool {
let s = v.to_string().to_lowercase();
v.get("bridgeConnected").and_then(|b| b.as_bool()) == Some(false)
|| s.contains("not connected")
|| s.contains("no client")
|| s.contains("offline")
|| s.contains("timeout")
}
/// Deliver the footprint to `target` ("kicad" | "fusion360" | "altium" | "orcad").
/// Returns a JSON result for the UI toast.
pub fn deliver(footprint_name: &str, kicad_mod: &str, target: &str) -> Value {
if kicad_mod.is_empty() {
return json!({ "ok": false, "error": "no footprint loaded" });
}
let dir = std::env::temp_dir().join(format!("adom-footprint-deliver-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let safe = footprint_name.replace(['/', ' '], "_");
let mod_path = dir.join(format!("{safe}.kicad_mod"));
if std::fs::write(&mod_path, kicad_mod).is_err() {
return json!({ "ok": false, "error": "could not write temp footprint" });
}
if target == "kicad" {
let send = ad("send_files", json!({ "filePaths": [mod_path.to_string_lossy()], "targetApp": "kicad", "destinationFolder": "footprints" }));
let inst = ad("kicad_install_library", json!({ "libraryPath": mod_path.to_string_lossy(), "libraryName": footprint_name, "libraryType": "footprint" }));
return summarize("KiCad", "footprint", &[send, inst]);
}
let (tool, app, folder) = match target {
"fusion360" | "fusion" => ("Fusion 360", "fusion360", "fusion"),
"altium" => ("Altium", "altium", "altium"),
"orcad" => ("OrCAD", "orcad", "orcad"),
_ => return json!({ "ok": false, "error": format!("unknown target {target}") }),
};
// Build a true .lbr via adom-lbr (synthesizes a symbol from the pads so the
// deviceset is placeable in the EAGLE-family tool).
let lbr_path = dir.join(format!("{safe}.lbr"));
match Command::new("adom-lbr")
.args(["generate", "--fp"]).arg(&mod_path)
.args(["--name", footprint_name])
.arg("--output").arg(&lbr_path)
.output()
{
Ok(o) if o.status.success() => {}
Ok(o) => return json!({ "ok": false, "error": "adom-lbr generate failed", "detail": String::from_utf8_lossy(&o.stderr) }),
Err(e) => return json!({ "ok": false, "error": format!("adom-lbr not available: {e}") }),
}
if app == "altium" || app == "orcad" {
return bridge_not_built(tool, &lbr_path.to_string_lossy());
}
let send = ad("send_files", json!({ "filePaths": [lbr_path.to_string_lossy()], "targetApp": app, "destinationFolder": folder }));
let win = send.as_ref().ok()
.and_then(|s| s.get("destinationPaths").and_then(|p| p.as_array()).and_then(|a| a.first()).and_then(|x| x.as_str()).map(String::from))
.unwrap_or_else(|| lbr_path.to_string_lossy().to_string());
let mut steps = vec![send];
if app == "fusion360" {
steps.push(ad("fusion_open_lbr", json!({ "filePath": win })));
}
summarize(tool, "library (.lbr)", &steps)
}
/// Altium / OrCAD have no Adom Desktop bridge yet — the .lbr is built, but invite
/// the user to build the bridge and contribute it on the wiki.
fn bridge_not_built(tool: &str, file: &str) -> Value {
json!({
"ok": false,
"needsBridge": true,
"target": tool,
"file": file,
"contribute": "https://wiki.adom.inc/adom/adom-desktop",
"message": format!("Your EAGLE .lbr for {tool} is built and saved — but the {tool} ↔ Adom Desktop bridge isn't built yet. Want {tool} connectivity in the Adom ecosystem? Be the one to build the bridge and contribute it on the wiki.")
})
}
fn summarize(tool: &str, what: &str, results: &[Result<Value, String>]) -> Value {
for r in results {
match r {
Err(e) => return json!({ "ok": false, "target": tool, "error": e, "offline": e.contains("unreachable") }),
Ok(v) if looks_offline(v) => {
return json!({ "ok": false, "target": tool, "offline": true,
"message": format!("File generated, but Adom Desktop isn't connected to {tool}. Open Adom Desktop on your laptop and try again.") });
}
_ => {}
}
}
json!({ "ok": true, "target": tool, "message": format!("Delivered {what} to {tool}.") })
}