app
Board to Molecule
Public Made by Adomby adom
Turn any KiCad, Fusion/EAGLE, or Altium board into an Adom molecule: wings plus MP1-MP4 machine pins, written back in the board's own format.
main
Colby Knox
board-to-molecule 1.0.0: Rust source, six before/after examples (KiCad, Fusion, Altium), Adom 3D viewer readme
eb9ea91
4d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
//! Board model -> preview .kicad_pcb (KiCad 9+ format).
//!
//! This is what lets one renderer serve all three EDAs: a Fusion or Altium board is
//! rebuilt as a KiCad board carrying its outline, holes, pads, tracks, vias, the
//! machine pins, and any embedded component models, all inside the one file, so
//! service-kicad can export a STEP that step2glb then anchors. It is a preview, not a
//! design translation: no nets, zones, or rules.
use crate::embed;
use crate::kicad::pin_step;
use crate::model::{rot, Board, Component, PadShape, Seg, Shape, Side};
use anyhow::Result;
use std::fmt::Write;
fn f(v: f64) -> String {
let s = format!("{v:.5}");
let s = s.trim_end_matches('0').trim_end_matches('.');
if s == "-0" { "0".into() } else { s.into() }
}
fn u() -> String {
format!("(uuid \"{}\")", uuid::Uuid::new_v4())
}
/// Machine-pin footprint name for an MP component, if it is one.
pub fn pin_kind(c: &Component) -> Option<&'static str> {
let is_ref = matches!(c.r#ref.as_str(), "MP1" | "MP2" | "MP3" | "MP4");
let p = c.package.to_ascii_uppercase();
if !is_ref || !(p.contains("MACH_PIN") || p.contains("MACHINEPIN") || p.contains("PIN_LRG") || p.contains("PIN_MED")) {
return None;
}
Some(if p.contains("LRG") || p.contains("LARGE") { "MachinePinLargeShort" } else { "MachinePinMediumShort" })
}
fn edge(out: &mut String, s: &Shape, layer: &str) {
match s {
Shape::Seg(Seg { x1, y1, x2, y2, mid: None, .. }) => {
let _ = writeln!(out, "\t(gr_line (start {} {}) (end {} {}) (stroke (width 0.1) (type solid)) (layer \"{layer}\") {})", f(*x1), f(-y1), f(*x2), f(-y2), u());
}
Shape::Seg(Seg { x1, y1, x2, y2, mid: Some(m), .. }) => {
let _ = writeln!(out, "\t(gr_arc (start {} {}) (mid {} {}) (end {} {}) (stroke (width 0.1) (type solid)) (layer \"{layer}\") {})", f(*x1), f(-y1), f(m.0), f(-m.1), f(*x2), f(-y2), u());
}
Shape::Circle(c) => {
let _ = writeln!(out, "\t(gr_circle (center {} {}) (end {} {}) (stroke (width 0.1) (type solid)) (fill no) (layer \"{layer}\") {})", f(c.cx), f(-c.cy), f(c.cx + c.r), f(-c.cy), u());
}
Shape::Poly(p) => {
let pts: String = p.iter().map(|(x, y)| format!("(xy {} {}) ", f(*x), f(-y))).collect();
let _ = writeln!(out, "\t(gr_poly (pts {pts}) (stroke (width 0.1) (type solid)) (fill no) (layer \"{layer}\") {})", u());
}
}
}
pub fn write(b: &Board) -> Result<String> {
let mut o = String::with_capacity(1 << 20);
o.push_str("(kicad_pcb (version 20241229) (generator \"board-to-molecule\") (generator_version \"1.0\")\n");
o.push_str("\t(general (thickness 1.6) (legacy_teardrops no))\n\t(paper \"A3\")\n");
o.push_str("\t(layers (0 \"F.Cu\" signal) (2 \"B.Cu\" signal) (9 \"F.Adhes\" user \"F.Adhesive\") (11 \"B.Adhes\" user \"B.Adhesive\") (13 \"F.Paste\" user) (15 \"B.Paste\" user) (5 \"F.SilkS\" user \"F.Silkscreen\") (7 \"B.SilkS\" user \"B.Silkscreen\") (1 \"F.Mask\" user) (3 \"B.Mask\" user) (25 \"Edge.Cuts\" user) (31 \"F.CrtYd\" user \"F.Courtyard\") (29 \"B.CrtYd\" user \"B.Courtyard\") (35 \"F.Fab\" user) (33 \"B.Fab\" user))\n");
o.push_str("\t(setup (pad_to_mask_clearance 0))\n\t(net 0 \"\")\n");
for s in &b.outline {
edge(&mut o, s, "Edge.Cuts");
}
let mut embedded: Vec<(String, Vec<u8>)> = vec![];
let mut pins_used: Vec<&str> = vec![];
for c in &b.components {
let layer = if c.side == Side::Bottom { "B.Cu" } else { "F.Cu" };
let _ = writeln!(o, "\t(footprint \"Preview:{}\" (layer \"{layer}\") {} (at {} {} {})", c.package.replace('"', "'"), u(), f(c.x), f(-c.y), f(c.rot));
let _ = writeln!(o, "\t\t(property \"Reference\" \"{}\" (at 0 0 0) (layer \"F.Fab\") (hide yes) {} (effects (font (size 0.5 0.5) (thickness 0.08))))", c.r#ref.replace('"', "'"), u());
let _ = writeln!(o, "\t\t(property \"Value\" \"{}\" (at 0 0 0) (layer \"F.Fab\") (hide yes) {} (effects (font (size 0.5 0.5) (thickness 0.08))))", c.value.replace('"', "'"), u());
for (i, p) in c.pads.iter().enumerate() {
// Pad position in the footprint frame (KiCad stores the pad angle absolute).
let (lx, ly) = rot(p.x - c.x, p.y - c.y, -c.rot);
let shape = match p.shape {
PadShape::Rect => "rect",
PadShape::Circle => "circle",
PadShape::Oval => "oval",
PadShape::RoundRect => "roundrect",
};
let (kind, layers, drill) = if p.drill > 0.0 {
("thru_hole", "\"*.Cu\" \"*.Mask\"".to_string(), format!(" (drill {})", f(p.drill.min(p.w.min(p.h) - 0.05).max(0.1))))
} else if p.side == Side::Bottom {
("smd", "\"B.Cu\" \"B.Mask\"".to_string(), String::new())
} else {
("smd", "\"F.Cu\" \"F.Mask\"".to_string(), String::new())
};
let rr = if p.shape == PadShape::RoundRect { " (roundrect_rratio 0.25)" } else { "" };
let _ = writeln!(
o,
"\t\t(pad \"{}\" {kind} {shape} (at {} {} {}) (size {} {}){drill} (layers {layers}){rr} {})",
if p.name.is_empty() { (i + 1).to_string() } else { p.name.replace('"', "'") },
f(lx), f(-ly), f(p.rot), f(p.w.max(0.05)), f(p.h.max(0.05)), u()
);
}
if let Some(pin) = pin_kind(c) {
let _ = writeln!(o, "\t\t(model \"kicad-embed://{pin}.step\" (offset (xyz 0 0 0)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 0)))");
if !pins_used.contains(&pin) {
pins_used.push(pin);
}
}
for body in &c.bodies {
let name = format!("{}.step", body.model);
if let Some(data) = b.models.get(&body.model) {
if !embedded.iter().any(|(n, _)| n == &name) {
embedded.push((name.clone(), data.clone()));
}
let _ = writeln!(
o,
"\t\t(model \"kicad-embed://{name}\" (offset (xyz {} {} {})) (scale (xyz 1 1 1)) (rotate (xyz {} {} {})))",
f(body.offset.0), f(body.offset.1), f(body.offset.2), f(body.rot.0), f(body.rot.1), f(body.rot.2)
);
}
}
o.push_str("\t)\n");
}
for (side, tracks) in [("F.Cu", &b.tracks_top), ("B.Cu", &b.tracks_bottom)] {
for t in tracks.iter() {
match t.mid {
None => {
let _ = writeln!(o, "\t(segment (start {} {}) (end {} {}) (width {}) (layer \"{side}\") (net 0) {})", f(t.x1), f(-t.y1), f(t.x2), f(-t.y2), f(t.width.max(0.05)), u());
}
Some(m) => {
let _ = writeln!(o, "\t(arc (start {} {}) (mid {} {}) (end {} {}) (width {}) (layer \"{side}\") (net 0) {})", f(t.x1), f(-t.y1), f(m.0), f(-m.1), f(t.x2), f(-t.y2), f(t.width.max(0.05)), u());
}
}
}
}
for v in &b.vias {
let _ = writeln!(o, "\t(via (at {} {}) (size {}) (drill {}) (layers \"F.Cu\" \"B.Cu\") (net 0) {})", f(v.x), f(-v.y), f(v.dia.max(v.drill + 0.1)), f(v.drill), u());
}
if !pins_used.is_empty() || !embedded.is_empty() {
o.push_str("\t(embedded_files\n");
for p in &pins_used {
o.push_str(&embed::file_entry(&format!("{p}.step"), pin_step(p), "\t\t"));
o.push('\n');
}
for (n, d) in &embedded {
o.push_str(&embed::file_entry(n, d, "\t\t"));
o.push('\n');
}
o.push_str("\t)\n");
}
o.push_str(")\n");
Ok(o)
}