app
Adom Footprint
Public Made by Adomby adom
KiCad footprint creator with layer HUD viewer and delivery to KiCad/Fusion 360
← Commit history
1.0.23: heatsinkVias in the create JSON expand to plated thru_hole pads numbered like the exposed pad (0.3 mm drill, 0.8 mm pitch, 0.15 mm ring, tented); HEATSINK VIAS MISSING hint for a big exposed pad without them (John, 2026-09-19)
6 files changed
+83−8
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-footprint"-version = "1.0.22"+version = "1.0.23" edition = "2021" description = "Create, preview, lint, and deliver KiCad footprints (.kicad_mod) with interactive FpView viewer"
SKILL.md+10@@ -27,6 +27,16 @@ Node.js server (footprint-creator/) +-- GET /health -> service health ``` +**Heatsink vias (1.0.23).** `"heatsinkVias": {"pad": "B4", "drill_mm": 0.3, "pitch_mm": 0.8,+"ring_mm": 0.15, "tented": true}` in the create JSON (only `pad` is required; those are the+defaults) writes plated `thru_hole` pads numbered like the exposed pad on a centred grid that+keeps a full ring inside it. Same-number plated holes are how KiCad, Altium (through adom-lbr)+and Fusion/EAGLE all express "same net, drilled", so the vias are drawn ONCE and reach every+tool. `create` prints HEATSINK VIAS MISSING when the largest pad is a big exposed pad without+them (John, 2026-09-19: a thermal pad without vias is a part that overheats). A wiki component+page keeps the same rule in `artwork/<MPN>-heatsink-via.json` and shows the vias in its hero+GLB as the `Adom.HeatsinkVias` layer (65 % transparent copper through a 1.6 mm board).+ ## Quick start ```bash
VERSION+1−1@@ -1 +1 @@-1.0.22+1.0.23
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-footprint",- "version": "1.0.22",+ "version": "1.0.23", "type": "app", "description": "KiCad footprint creator with layer HUD viewer and delivery to KiCad/Fusion 360", "dependencies": {
page.json+3−3@@ -1,6 +1,6 @@ { "slug": "adom-footprint",- "version": "1.0.22",+ "version": "1.0.23", "type": "app", "description": "KiCad footprint creator with layer HUD viewer and delivery to KiCad/Fusion 360", "dependencies": {@@ -48,7 +48,7 @@ "send footprint to fusion", "kicad footprint for this chip" ],- "discovery_pitch": "Create, view and edit KiCad PCB footprints in a live layer viewer — auto-placed RefDes/Value, auto-generated heatsink vias, heatsink-aware solder paste, and one-click delivery to KiCad/Fusion/Altium/OrCAD.",+ "discovery_pitch": "Create, view and edit KiCad PCB footprints in a live layer viewer \u2014 auto-placed RefDes/Value, auto-generated heatsink vias, heatsink-aware solder paste, and one-click delivery to KiCad/Fusion/Altium/OrCAD.", "sample_prompts": [ { "label": "Make a footprint",@@ -67,4 +67,4 @@ "prompt": "Auto-place the RefDes and Value clear of the pads and save it" } ]-}+}
src/main.rs+67−2@@ -143,9 +143,72 @@ struct CreateInput { /// two-pad parts, true otherwise. #[serde(default)] polarized: Option<bool>,+ /// Heatsink (thermal) vias under an exposed pad, written INTO the footprint as+ /// plated thru_hole pads that carry the exposed pad's own number, on a centred grid+ /// that keeps a full annular ring inside the pad: `{"pad": "B4", "drill_mm": 0.3,+ /// "pitch_mm": 0.8, "ring_mm": 0.15, "tented": true}` (those are the defaults, only+ /// `pad` is required). Same-number plated holes are how KiCad, Altium (via adom-lbr)+ /// and Fusion/EAGLE all say "same net, drilled", so nobody redraws them per tool+ /// (John, 2026-09-19). The wiki's ::adom-heatsink-via widget shows the same rule.+ #[serde(default, rename = "heatsinkVias", alias = "heatsink_vias")]+ heatsink_vias: Option<HeatsinkVias>, } -#[derive(serde::Deserialize)]+#[derive(serde::Deserialize, Clone)]+struct HeatsinkVias {+ pad: String,+ #[serde(default = "hv_drill", alias = "drill")]+ drill_mm: f64,+ #[serde(default = "hv_pitch", alias = "pitch")]+ pitch_mm: f64,+ #[serde(default = "hv_ring", alias = "ring")]+ ring_mm: f64,+ #[serde(default = "hv_tented")]+ tented: bool,+}+fn hv_drill() -> f64 { 0.3 }+fn hv_pitch() -> f64 { 0.8 }+fn hv_ring() -> f64 { 0.15 }+fn hv_tented() -> bool { true }++/// Expand `heatsinkVias` into thru_hole pads numbered like the exposed pad.+fn expand_heatsink_vias(input: &mut CreateInput) -> usize {+ let Some(hv) = input.heatsink_vias.clone() else { return 0 };+ let Some(ep) = input.pads.iter().find(|p| p.number == hv.pad && p.mount.as_deref().unwrap_or("smd") == "smd").cloned() else { return 0 };+ let dia = hv.drill_mm + 2.0 * hv.ring_mm;+ let nx = (((ep.dx - dia) / hv.pitch_mm).floor() as i64 + 1).max(1);+ let ny = (((ep.dy - dia) / hv.pitch_mm).floor() as i64 + 1).max(1);+ let mut n = 0;+ for j in 0..ny {+ for i in 0..nx {+ let x = ep.x + (i as f64 - (nx - 1) as f64 / 2.0) * hv.pitch_mm;+ let y = ep.y + (j as f64 - (ny - 1) as f64 / 2.0) * hv.pitch_mm;+ input.pads.push(JsonPad { number: hv.pad.clone(), x, y, dx: dia, dy: dia, shape: Some("circle".into()), angle: None,+ layers: Some(if hv.tented { "*.Cu".into() } else { "*.Cu *.Mask".into() }), mount: Some("thru_hole".into()), drill: Some(hv.drill_mm) });+ n += 1;+ }+ }+ n+}++/// The exposed-pad check every footprint gets: a big pad with no vias into the plane is a+/// part that overheats, and a user who has to redraw the vias per EDA tool will not.+fn heatsink_hint(input: &CreateInput, added: usize) {+ let smd: Vec<(&str, f64)> = input.pads.iter().filter(|p| p.mount.as_deref().unwrap_or("smd") == "smd").map(|p| (p.number.as_str(), p.dx * p.dy)).collect();+ if smd.is_empty() { return; }+ let mut areas: Vec<f64> = smd.iter().map(|(_, a)| *a).collect(); areas.sort_by(|a, b| a.partial_cmp(b).unwrap());+ let median = areas[areas.len() / 2]; let max = *areas.last().unwrap();+ let Some((big, area)) = smd.iter().find(|(_, a)| *a == max) else { return };+ if area < &1.0 || area < &(3.0 * median) { return; }+ let th = input.pads.iter().filter(|p| p.number == *big && p.mount.as_deref() == Some("thru_hole")).count();+ if th > 0 {+ println!("_hint: heatsink vias: {th} plated holes numbered {big} under the {area:.1} mm2 exposed pad ({added} from heatsinkVias); they carry to Altium and Fusion through adom-lbr unchanged, and the hero GLB shows them as the Adom.HeatsinkVias layer.");+ } else {+ println!("_hint: HEATSINK VIAS MISSING: pad {big} is a {area:.1} mm2 exposed pad with no plated holes under it. Add `\"heatsinkVias\": {{\"pad\": \"{big}\"}}` (defaults: 0.3 mm drill, 0.8 mm pitch, 0.15 mm ring, tented) and re-run; the vias then reach KiCad, Altium and Fusion from this one footprint, and the wiki component page's artwork/<MPN>-heatsink-via.json and hero GLB layer come from the same numbers.");+ }+}++#[derive(serde::Deserialize, Clone)] struct JsonPad { number: String, x: f64,@@ -297,7 +360,9 @@ fn cmd_create(file: Option<PathBuf>, out: Option<PathBuf>) -> Result<()> { s } };- let input: CreateInput = serde_json::from_str(&body).context("parsing create JSON")?;+ let mut input: CreateInput = serde_json::from_str(&body).context("parsing create JSON")?;+ let added_vias = expand_heatsink_vias(&mut input);+ heatsink_hint(&input, added_vias); if input.footprint_name.is_empty() { return Err(anyhow!("footprintName is required")); }