app
adom-layout-viewer
Public Made by Adomby adom
Interactive PCB layout viewer: your EDA's own render plus live net/trace highlighting and per-pad stock, price and wiki lookup
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
//! adom-layout-viewer — standalone Rust CLI + live app for whole KiCad PCBs.
//!
//! Sibling of adom-symbol / adom-footprint, but for a whole `.kicad_pcb`. The
//! headline feature is REAL net/trace highlighting + per-component hover info,
//! which is only possible because we self-render an identity-tagged SVG
//! (`data-net`/`data-ref`/`data-pad`) instead of consuming kicad-cli's flat,
//! anonymous export. Meant to be embedded as a live PCB "view" in any app.
mod pcb_render;
mod kicad;
mod eagle;
mod altium;
mod sexpr;
mod sch_render;
mod server;
mod enrich;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::fs;
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "adom-layout-viewer", version, about = "Interactive PCB layout viewer (.kicad_pcb): net/trace highlighting, component outlines + hover, per-side layers")]
struct Cli {
#[command(subcommand)]
command: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Render a `.kicad_pcb` to a themed, identity-tagged SVG (+ optional -meta.json).
Render {
#[arg(long)]
file: PathBuf,
#[arg(long)]
out: PathBuf,
/// Also write <out>.meta.json (nets, components, counts)
#[arg(long)]
meta: bool,
/// EDA-exported SVG to use as the visual base (e.g. Altium's render/pcb.svg)
#[arg(long)]
base: Option<PathBuf>,
},
/// Export a self-contained INTERACTIVE HTML (pan/zoom + net/trace highlight +
/// component hover) for embedding as a PCB "view" in any app via <iframe>.
Embed {
#[arg(long)]
file: PathBuf,
#[arg(long)]
out: Option<PathBuf>,
/// EDA-exported SVG to use as the visual base (e.g. Altium's render/pcb.svg)
#[arg(long)]
base: Option<PathBuf>,
},
/// [dev] Render a `.kicad_sch` to a themed identity-tagged SVG (adom-schematic engine).
RenderSch {
#[arg(long)]
file: PathBuf,
#[arg(long)]
out: PathBuf,
#[arg(long)]
meta: bool,
},
/// Run the live app server (AI-drivable; the embeddable live PCB view).
Serve {
/// Optionally load a `.kicad_pcb` at startup.
#[arg(long)]
file: Option<PathBuf>,
#[arg(long, default_value_t = 8784)]
port: u16,
},
/// Drive the running server's front-end (the AI's live UI channel) as a user's
/// click would: `ui toast "done"` · `ui fit` · `ui net GND` · `ui clear`.
Ui { /// toast | fit | net | clear
action: String,
/// message (toast) or argument (net name)
arg: Option<String>,
#[arg(long = "type", default_value = "info")] ty: String,
#[arg(long, default_value_t = 8784)] port: u16 },
/// Check the live server is up on --port (prints OK:/ERROR: + a Hint:).
Health { #[arg(long, default_value_t = 8784)] port: u16 },
/// Install this binary to ~/.local/bin so host apps can shell it by name.
Install,
}
const APP_HTML: &str = include_str!("app.html");
const VIEWER_JS: &str = include_str!("viewer.js");
/// Build the self-contained embed HTML: inline viewer.js + the board payload in
/// place of the live `<script src="/viewer.js">` tag.
pub fn build_embed(svg: &str, meta_json: &str) -> String {
let payload = format!(
"<script>window.__PCB__={{svg:{},meta:{}}};</script>\n<script>{}</script>",
serde_json::to_string(svg).unwrap_or_else(|_| "\"\"".into()),
meta_json,
VIEWER_JS
);
APP_HTML.replace(r#"<script src="viewer.js"></script>"#, &payload)
}
// CLI contract: every response starts with `OK:` or `ERROR:`, and every error
// carries an actionable `Hint:` naming the next command.
fn main() {
if let Err(e) = run() {
println!("ERROR: {e}");
println!("Hint: check the file path / arguments; run `adom-layout-viewer --help`");
std::process::exit(1);
}
}
fn run() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Cmd::Render { file, out, meta, base } => {
let raw = fs::read(&file).with_context(|| format!("read {}", file.display()))?;
let base_svg = base.as_ref().and_then(|p| fs::read_to_string(p).ok());
let (svg, m) = pcb_render::render_board_bytes_with(&raw, base_svg.as_deref());
fs::write(&out, &svg).with_context(|| format!("write {}", out.display()))?;
println!(
"OK: rendered {} -> {} ({} nets, {} comps, {} tracks, {} vias, {} pads, {:.1}x{:.1}mm)",
file.display(), out.display(), m.net_names.len(), m.components.len(),
m.track_count, m.via_count, m.pad_count, m.width_mm, m.height_mm
);
if meta {
let mp = out.with_extension("meta.json");
fs::write(&mp, serde_json::to_string_pretty(&m)?)?;
println!("OK: meta -> {}", mp.display());
}
}
Cmd::Embed { file, out, base } => {
let raw = fs::read(&file).with_context(|| format!("read {}", file.display()))?;
let base_svg = base.as_ref().and_then(|p| fs::read_to_string(p).ok());
let (svg, m) = pcb_render::render_board_bytes_with(&raw, base_svg.as_deref());
let html = build_embed(&svg, &serde_json::to_string(&m)?);
let out = out.unwrap_or_else(|| file.with_extension("pcbview.html"));
fs::write(&out, &html).with_context(|| format!("write {}", out.display()))?;
println!("OK: embed -> {} ({} comps, {} nets)", out.display(), m.components.len(), m.net_names.len());
}
Cmd::RenderSch { file, out, meta } => {
let content = fs::read_to_string(&file).with_context(|| format!("read {}", file.display()))?;
let sheet = sch_render::parse(&content);
let (svg, m) = sch_render::render(&sheet);
fs::write(&out, &svg)?;
eprintln!("[adom-schematic] {} → {} ({} comps, {} wires, {} nets, {:.0}×{:.0}mm)",
file.display(), out.display(), m.components.len(), m.track_count, m.net_names.len(), m.width_mm, m.height_mm);
if meta { let mp = out.with_extension("meta.json"); fs::write(&mp, serde_json::to_string_pretty(&m)?)?; }
let emb = out.with_extension("embed.html");
fs::write(&emb, build_embed(&svg, &serde_json::to_string(&m)?))?;
eprintln!("[adom-schematic] embed → {}", emb.display());
}
Cmd::Serve { file, port } => {
if let Some(f) = file {
let content = fs::read_to_string(&f).with_context(|| format!("read {}", f.display()))?;
let name = f.file_stem().and_then(|s| s.to_str()).unwrap_or("board").to_string();
server::load_pcb(&name, &content)?;
println!("OK: loaded {}", f.display());
}
server::run(port)?;
}
Cmd::Ui { action, arg, ty, port } => {
let base = format!("http://127.0.0.1:{port}");
let (path, payload) = match action.as_str() {
"toast" => ("/ui/toast", serde_json::json!({ "message": arg.unwrap_or_default(), "type": ty }).to_string()),
"fit" | "clear" => ("/ui/cmd", serde_json::json!({ "action": action }).to_string()),
"net" => ("/ui/cmd", serde_json::json!({ "action": "net", "arg": arg.unwrap_or_default() }).to_string()),
other => {
println!("ERROR: unknown ui action '{other}'");
println!("Hint: use one of: toast <msg> | fit | net <name> | clear");
return Ok(());
}
};
match ureq::post(&format!("{base}{path}")).set("Content-Type", "application/json").send_string(&payload) {
Ok(_) => println!("OK: ui {action} sent to {base}"),
Err(_) => {
println!("ERROR: could not reach the viewer server on port {port}");
println!("Hint: start it with `adom-layout-viewer serve --port {port} &`");
}
}
}
Cmd::Health { port } => match ureq::get(&format!("http://127.0.0.1:{port}/version")).call() {
Ok(r) => println!("OK: adom-layout-viewer healthy on port {port} ({})", r.into_string().unwrap_or_default().trim()),
Err(_) => {
println!("ERROR: no adom-layout-viewer server responding on port {port}");
println!("Hint: start it with `adom-layout-viewer serve --port {port} &`");
}
},
Cmd::Install => {
let exe = std::env::current_exe()?;
let home = std::env::var("HOME").unwrap_or_default();
let bin = format!("{home}/.local/bin");
fs::create_dir_all(&bin)?;
let dst = format!("{bin}/adom-layout-viewer");
fs::copy(&exe, &dst)?;
println!("OK: installed adom-layout-viewer to {dst}");
println!("Hint: ensure ~/.local/bin is on your PATH");
}
}
Ok(())
}