← 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)

John Lauer ·d9a366d008 ·1mo ago ·parent 4ab1cdf
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"     }   ]-}\ No newline at end of file+}
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"));     }