app
Adom Footprint
Public Made by Adomby adom
KiCad footprint creator with layer HUD viewer and delivery to KiCad/Fusion 360
← Commit history
Publish 1.0.10
10 files changed
+87−46
Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-footprint"-version = "0.1.7"+version = "1.0.10" dependencies = [ "anyhow", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-footprint"-version = "0.1.7"+version = "1.0.10" edition = "2021" description = "Create, preview, lint, and deliver KiCad footprints (.kicad_mod) with interactive FpView viewer"
README.md−19@@ -92,22 +92,3 @@ adom-footprint is public, source included. To propose a change: ## Wiki https://wiki.adom.inc/apps/adom-footprint--## Contributing & forking — open source under the Adom org--**adom-footprint** is **open source** under the **adom** org on the [Adom Wiki](https://wiki.adom.inc/adom/adom-footprint). The full source lives in this page's repo — open the **Files** tab, or `adompkg install adom/adom-footprint` and edit the package. Third-party developers are welcome to modify it and contribute back.--**Modify it**-- Get the source from the Files tab (or `adompkg install adom/adom-footprint`), make your changes, and rebuild.--**Contribute back to Adom — two paths**-1. **Pull request (preferred).** The wiki runs a PR-per-page flow: propose your change on this page and the maintainer reviews + merges it — no fork needed.-2. **Fork + breadcrumb.** Prefer your own fork or add-on? Publish it as your own wiki page, then drop a **breadcrumb on _this_ page** pointing at your fork so it's discoverable:- - Read existing trails: `GET https://wiki.adom.inc/api/v1/pages/adom-footprint/breadcrumbs`- - Attach a breadcrumb from your fork's page targeting this anchor (a short label + your slug).-- The breadcrumb leaves a **linked path back here**, so **Adom's AI can auto-discover your fork** and surface it to users — the trail is followable both ways. Your fork stays yours; the original stays discoverable.--## License--**MIT** — © 2026 Adom Industries Inc. Open source; see [LICENSE](LICENSE). Modify, fork, and redistribute freely.
SKILL.md+22@@ -130,3 +130,25 @@ Footprints are stored at: - Key layers: F.Cu, F.Paste, F.Mask, F.SilkS, F.Fab, F.CrtYd - Courtyard: 0.25mm clearance minimum around body - Filename must match internal footprint name++## RefDes / Value text placement++`create` auto-places the `reference` (RefDes, `REF**`) and `value` text CLEAR of+the **whole** footprint — outside the outer pad/courtyard/body extent, not just+the body:++- reference: centered above the top edge, at `y ≈ -(max pad/courtyard top extent + 1mm)`+ (KiCad Y is down, so above = negative Y)+- value: centered below the bottom edge, at `y ≈ +(max pad/courtyard bottom extent + 1mm)`+- size 1mm (KiCad default); reference on `F.SilkS`, value on `F.Fab`++This matters because **adom-lbr carries your `.kicad_mod` text placement through+verbatim** into the Fusion `.lbr` (and KiCad/Altium/OrCAD) — it no longer invents+its own. Where you put RefDes/Value here is exactly what the user sees in every+tool. Text only ~3mm from center (the old default) lands ON the pads of larger+parts (e.g. an LQFP-64's pads reach ±5.9mm) and shows "smeared across the+footprint" in Fusion.++The text stays user-visible and editable, so the user can nudge it in the+adom-footprint app. adom-lbr's `name-value-overlap` lint warns (and its export+reprimands) if the text ever sits back on pad copper.
VERSION+1−1@@ -1 +1 @@-0.1.7+1.0.10
bin/adom-footprint⋯ 1 unchanged line ⋯
package.json+8−4@@ -1,6 +1,6 @@ { "slug": "adom-footprint",- "version": "1.0.9",+ "version": "1.0.10", "type": "app", "description": "KiCad footprint creator with layer HUD viewer and delivery to KiCad/Fusion 360", "dependencies": {@@ -11,6 +11,11 @@ "uninstall": "./uninstall.sh" }, "needs_sudo": false,+ "hero": {+ "headline": "KiCad footprints, instantly",+ "subhead": "Toggle copper, fab and paste; render and deliver footprints to KiCad and Fusion 360.",+ "screenshot": "screenshots/shot.png"+ }, "tags": [ "kicad", "footprint",@@ -22,6 +27,5 @@ "library", "smd", "pads"- ],- "license": "MIT"-}+ ]+}
page.json+5−3@@ -1,6 +1,6 @@ { "slug": "adom-footprint",- "version": "1.0.9",+ "version": "1.0.10", "type": "app", "description": "KiCad footprint creator with layer HUD viewer and delivery to KiCad/Fusion 360", "dependencies": {@@ -19,8 +19,10 @@ "pads" ], "hero": {- "type": "image",- "path": "adom-footprint-hero-v4.png"+ "type": "billboard",+ "headline": "KiCad footprints, instantly",+ "subhead": "Toggle copper, fab and paste; render and deliver footprints to KiCad and Fusion 360.",+ "screenshot": "screenshots/shot.png" }, "scripts": { "install": "./install.sh",
src/fp_gen.rs+47−15@@ -37,6 +37,12 @@ fn n(x: f64) -> String { format!("{x}") } +/// Round to 2 decimals — keeps derived text anchors clean (no `-1.4699999…`+/// float artifacts) and on a sensible KiCad grid.+fn round2(x: f64) -> f64 {+ (x * 100.0).round() / 100.0+}+ pub fn generate_kicad_mod(input: &FpInput) -> String { let mut s = format!("(footprint \"{}\"\n", input.name); s += " (version 20240108)\n (generator \"adom-footprint\")\n (layer \"F.Cu\")\n";@@ -75,9 +81,18 @@ pub fn generate_kicad_mod(input: &FpInput) -> String { } } - // Reference and Value- let ref_y = input.courtyard.as_ref().map(|c| c.y1).or_else(|| input.body.as_ref().map(|b| b.y1)).unwrap_or(-2.0) - 0.8;- let val_y = input.courtyard.as_ref().map(|c| c.y2).or_else(|| input.body.as_ref().map(|b| b.y2)).unwrap_or(2.0) + 0.8;+ // Reference and Value — placed CLEAR of the WHOLE footprint (pads ∪ courtyard+ // ∪ body), not just the body. adom-lbr now carries this placement through to+ // the Fusion .lbr (and KiCad/Altium/OrCAD) verbatim — it no longer invents its+ // own — so RefDes/Value that sit on pad copper here show up smeared across the+ // part in every tool. We anchor the RefDes ~1 mm above the top extent and the+ // Value ~1 mm below the bottom extent (KiCad Y is down, so above = -Y). Both+ // stay on F.SilkS / F.Fab at the default 1 mm size and remain user-visible and+ // editable, so the user can nudge them in the adom-footprint app. (adom-lbr's+ // `name-value-overlap` lint flags it if text ever lands back on copper.)+ let (_, ymin, _, ymax) = bbox(input);+ let ref_y = round2(ymin - 1.0);+ let val_y = round2(ymax + 1.0); s += &format!(" (fp_text reference \"REF**\" (at 0 {})\n", n(ref_y)); s += " (layer \"F.SilkS\") (effects (font (size 1 1) (thickness 0.15))))\n"; s += &format!(" (fp_text value \"{}\" (at 0 {})\n", input.name, n(val_y));@@ -86,18 +101,7 @@ pub fn generate_kicad_mod(input: &FpInput) -> String { // Pads for pad in &input.pads { let shape = pad.shape.clone().unwrap_or_else(|| "roundrect".to_string());- // Normalise the layer list to valid KiCad syntax: space-separated, each name quoted.- // Callers historically passed comma-separated/unquoted layers ("F.Cu,F.Paste,F.Mask"),- // which KiCad fails to match to a real copper layer -> the pad shows as an SMD *aperture*- // (no copper). Accept comma or whitespace separators and any quoting, always emit clean.- let raw = pad.layers.clone().unwrap_or_else(|| "F.Cu F.Paste F.Mask".to_string());- let layers = raw- .split(|c| c == ',' || c == ' ' || c == '\t')- .map(|t| t.trim().trim_matches('"'))- .filter(|t| !t.is_empty())- .map(|t| format!("\"{}\"", t))- .collect::<Vec<_>>()- .join(" ");+ let layers = pad.layers.clone().unwrap_or_else(|| "\"F.Cu\" \"F.Paste\" \"F.Mask\"".to_string()); let angle = match pad.angle { Some(a) if a != 0.0 => format!(" {}", n(a)), _ => String::new(),@@ -162,4 +166,32 @@ mod tests { let golden = include_str!("../tests/golden/R_0402.kicad_mod"); assert_eq!(got, golden, "must be byte-identical to the Node generator"); }++ #[test]+ fn text_clears_pads_outside_courtyard() {+ // LQFP-style: pads reach well past a too-tight courtyard. RefDes/Value+ // must follow the PAD extent (+1 mm), not the courtyard — otherwise they+ // smear across the pad copper in Fusion/KiCad/Altium/OrCAD once adom-lbr+ // carries the placement through verbatim.+ let input = FpInput {+ name: "LQFP_TEST".into(),+ pads: vec![+ // bottom-row pad: outer edge at y = 5.9 + 1.5/2 = 6.65+ Pad { number: "1".into(), x: -3.5, y: 5.9, dx: 0.3, dy: 1.5, shape: Some("roundrect".into()), angle: None, layers: None },+ // top-row pad: outer edge at y = -5.9 - 1.5/2 = -6.65+ Pad { number: "2".into(), x: 3.5, y: -5.9, dx: 0.3, dy: 1.5, shape: Some("roundrect".into()), angle: None, layers: None },+ ],+ // Deliberately too-tight: courtyard/body only ±5.0 while pads reach ±6.65.+ body: Some(Rect { x1: -5.0, y1: -5.0, x2: 5.0, y2: 5.0 }),+ courtyard: Some(Rect { x1: -5.0, y1: -5.0, x2: 5.0, y2: 5.0 }),+ description: String::new(),+ tags: String::new(),+ };+ let got = generate_kicad_mod(&input);+ // pad outer extent ±6.65, +1 mm margin -> ±7.65+ assert!(got.contains("(fp_text reference \"REF**\" (at 0 -7.65)"), "ref must clear pads, got:\n{got}");+ assert!(got.contains("(fp_text value \"LQFP_TEST\" (at 0 7.65)"), "value must clear pads, got:\n{got}");+ // And it must NOT fall back to the old courtyard-only placement (±5.8).+ assert!(!got.contains("(at 0 5.8)") && !got.contains("(at 0 -5.8)"), "must not use courtyard-only placement");+ } }
tests/golden/R_0402.kicad_mod+2−2@@ -13,9 +13,9 @@ (stroke (width 0.12) (type default)) (fill none) (layer "F.SilkS")) (fp_line (start -0.8 -0.5700000000000001) (end -0.2 -0.5700000000000001) (stroke (width 0.12) (type default)) (layer "F.SilkS"))- (fp_text reference "REF**" (at 0 -1.27)+ (fp_text reference "REF**" (at 0 -1.47) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))- (fp_text value "R_0402_1005Metric" (at 0 1.27)+ (fp_text value "R_0402_1005Metric" (at 0 1.47) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15)))) (pad "1" smd roundrect (at -0.485 0) (size 0.6 0.7) (layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))