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
1.1.0: every molecule gets large machine pins on the 32 mm grid; gallery rebuilt; corrected notes on kicad-to-molecule
c794385
4d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
//! The molecule plan: the EDA-neutral hub every reader feeds and every writer consumes.
//!
//! Same idea as the adom-lbr JSON: one canonical description in the middle, so a
//! board read from KiCad, Fusion/EAGLE, or Altium turns into the same plan, and the
//! same plan can be written back into any of them.
//!
//! Frame: millimeters, Y up. "Front" is the low-Y edge, so MP1 (front-left) is the
//! bottom-left pin seen from above. Readers for Y-down formats (KiCad) flip on the
//! way in; writers flip on the way out.
use serde_json::{json, Value};
pub const SCHEMA: &str = "adom.board-to-molecule.plan/1";
#[derive(Debug, Clone, Copy)]
pub struct Spec {
pub label: &'static str,
pub grid_mm: f64,
pub edge_offset_mm: f64,
pub pad_mm: f64,
pub drill_mm: f64,
/// Package name in Adom_Library_Master (Fusion/EAGLE); step2glb reads the pin size from it.
pub eagle_package: &'static str,
/// Footprint name used for KiCad and Altium.
pub footprint: &'static str,
}
/// The one molecule rule: large machine pins, pin-to-pin span on the 32 mm grid,
/// pin centers 3 mm in from the board edge.
pub const PIN: Spec = Spec {
label: "large",
grid_mm: 32.0,
edge_offset_mm: 3.0,
pad_mm: 5.2,
drill_mm: 3.45,
eagle_package: "MACH_PIN_LRG_SHORT",
footprint: "MachinePinLargeShort",
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BBox {
pub xmin: f64,
pub ymin: f64,
pub xmax: f64,
pub ymax: f64,
}
impl BBox {
pub fn w(&self) -> f64 {
self.xmax - self.xmin
}
pub fn h(&self) -> f64 {
self.ymax - self.ymin
}
pub fn cx(&self) -> f64 {
(self.xmin + self.xmax) / 2.0
}
pub fn cy(&self) -> f64 {
(self.ymin + self.ymax) / 2.0
}
pub fn empty() -> BBox {
BBox { xmin: f64::INFINITY, ymin: f64::INFINITY, xmax: f64::NEG_INFINITY, ymax: f64::NEG_INFINITY }
}
pub fn add(&mut self, x: f64, y: f64) {
self.xmin = self.xmin.min(x);
self.ymin = self.ymin.min(y);
self.xmax = self.xmax.max(x);
self.ymax = self.ymax.max(y);
}
pub fn is_empty(&self) -> bool {
!self.xmin.is_finite()
}
pub fn json(&self) -> Value {
json!({"x": r6(self.xmin), "y": r6(self.ymin), "w": r6(self.w()), "h": r6(self.h())})
}
}
#[derive(Debug, Clone)]
pub struct Pin {
pub r#ref: String,
pub role: &'static str,
pub x: f64,
pub y: f64,
}
/// Adom convention: numbered counterclockwise seen from above, starting front-left.
pub const PIN_ROLES: [(&str, &str); 4] =
[("MP1", "front-left"), ("MP2", "back-left"), ("MP3", "back-right"), ("MP4", "front-right")];
#[derive(Debug, Clone)]
pub struct Plan {
pub eda: String,
pub source: String,
pub orig: BBox,
pub new: BBox,
pub pins: Vec<Pin>,
pub vcut: bool,
pub notes: Vec<String>,
}
impl Plan {
pub fn spec(&self) -> &'static Spec {
&PIN
}
pub fn to_json(&self) -> Value {
let s = self.spec();
json!({
"schema": SCHEMA,
"source": {"eda": self.eda, "file": self.source},
"units": "mm",
"frame": "y-up",
"orig_bbox": self.orig.json(),
"new_bbox": self.new.json(),
"size_label": s.label,
"grid_mm": s.grid_mm,
"edge_offset_mm": s.edge_offset_mm,
"pin": {"pad_mm": s.pad_mm, "drill_mm": s.drill_mm,
"eagle_package": s.eagle_package, "footprint": s.footprint},
"wing_x_mm": r6((self.new.w() - self.orig.w()) / 2.0),
"wing_y_mm": r6((self.new.h() - self.orig.h()) / 2.0),
"pins": self.pins.iter().map(|p| json!({"ref": p.r#ref, "role": p.role, "x": r6(p.x), "y": r6(p.y)})).collect::<Vec<_>>(),
"vcut": self.vcut,
"notes": self.notes,
})
}
/// Score lines (Y-up) along the original outline: two full-length cuts on the long
/// axis, two short ones between them, so the board snaps out of its wings.
pub fn vcut_lines(&self) -> Vec<[f64; 4]> {
let (o, n) = (&self.orig, &self.new);
if o.h() >= o.w() {
vec![
[o.xmin, n.ymin, o.xmin, n.ymax],
[o.xmax, n.ymin, o.xmax, n.ymax],
[o.xmin, o.ymin, o.xmax, o.ymin],
[o.xmin, o.ymax, o.xmax, o.ymax],
]
} else {
vec![
[n.xmin, o.ymin, n.xmax, o.ymin],
[n.xmin, o.ymax, n.xmax, o.ymax],
[o.xmin, o.ymin, o.xmin, o.ymax],
[o.xmax, o.ymin, o.xmax, o.ymax],
]
}
}
}
pub fn r6(v: f64) -> f64 {
let r = (v * 1e6).round() / 1e6;
if r == 0.0 { 0.0 } else { r }
}
/// Wings + four corner pins around `orig` (Y-up mm).
///
/// The corner-to-corner span snaps up to a whole number of grid steps, then the board
/// edge sits `edge_offset_mm` outside each pin. The new outline is centered on the
/// original, which never moves.
pub fn make_plan(eda: &str, source: &str, orig: BBox, vcut: bool) -> Plan {
let s = &PIN;
let snap = |v: f64| (s.grid_mm).max(((v / s.grid_mm * 1e9).round() / 1e9).ceil() * s.grid_mm);
let new_w = snap(orig.w()) + 2.0 * s.edge_offset_mm;
let new_h = snap(orig.h()) + 2.0 * s.edge_offset_mm;
let new = BBox {
xmin: orig.cx() - new_w / 2.0,
ymin: orig.cy() - new_h / 2.0,
xmax: orig.cx() + new_w / 2.0,
ymax: orig.cy() + new_h / 2.0,
};
let (l, r) = (new.xmin + s.edge_offset_mm, new.xmax - s.edge_offset_mm);
let (f, b) = (new.ymin + s.edge_offset_mm, new.ymax - s.edge_offset_mm);
let pins = PIN_ROLES
.iter()
.map(|(rf, role)| {
let (x, y) = match *role {
"front-left" => (l, f),
"back-left" => (l, b),
"back-right" => (r, b),
_ => (r, f),
};
Pin { r#ref: rf.to_string(), role, x, y }
})
.collect();
Plan { eda: eda.into(), source: source.into(), orig, new, pins, vcut, notes: vec![] }
}
#[cfg(test)]
mod tests {
use super::*;
fn bb(x: f64, y: f64, w: f64, h: f64) -> BBox {
BBox { xmin: x, ymin: y, xmax: x + w, ymax: y + h }
}
#[test]
fn large_pins_on_the_32mm_grid() {
// Span rounds up to whole 32 mm steps (at least one), plus 3 mm a side.
for (w, h, nw, nh) in [
(10.4, 10.4, 38.0, 38.0),
(25.4, 25.4, 38.0, 38.0),
(32.0, 32.0, 38.0, 38.0),
(32.1, 20.0, 70.0, 38.0),
(35.0, 85.0, 70.0, 102.0),
(136.2, 93.7, 166.0, 102.0),
] {
let p = make_plan("kicad", "t", bb(0.0, 0.0, w, h), false);
assert!((p.new.w() - nw).abs() < 1e-9 && (p.new.h() - nh).abs() < 1e-9, "{w}x{h} -> {}x{}", p.new.w(), p.new.h());
let span = p.pins[3].x - p.pins[0].x;
assert!((span / 32.0 - (span / 32.0).round()).abs() < 1e-9, "pin span {span} is not a 32 mm multiple");
assert_eq!(p.spec().eagle_package, "MACH_PIN_LRG_SHORT");
}
}
#[test]
fn pins_ccw_from_front_left() {
let p = make_plan("eagle", "t", bb(0.0, 0.0, 10.0, 10.0), false);
assert!(p.pins[0].x < p.pins[3].x && p.pins[0].y < p.pins[1].y);
assert_eq!(p.pins[1].x, p.pins[0].x);
assert_eq!(p.pins[2].y, p.pins[1].y);
}
}