--- a/crates/adom-aiflow/src/thermalvias.rs +++ b/crates/adom-aiflow/src/thermalvias.rs @@ -81,7 +81,8 @@ } f && bk }; - let tracks = b.tracks(); let vias = b.vias(); + // Planned holes are through vias: inner copper must remain an obstacle too. + let tracks = b.copper_tracks(); let vias = b.vias(); // holes: drill edge to drill edge at least the hole-to-hole minimum from every via and through-hole pad let h2h = aiflow_pours::hole_to_hole(spec); let mut holes = aiflow_pours::board_holes(b); @@ -272,6 +273,31 @@ #[cfg(test)] mod tests { + #[test] + fn through_thermal_vias_clear_tracks_on_every_inner_layer() { + let text = r#"(kicad_pcb (net 0 "") (net 1 "GND") (net 2 "SIGNAL") + (layers (0 "F.Cu" signal)(4 "In1.Cu" signal)(6 "In2.Cu" signal)(2 "B.Cu" signal)) + (gr_rect (start 0 0)(end 30 30)(layer "Edge.Cuts")) + (footprint "x" (layer "F.Cu") (at 10 10) (property "Reference" "U1") + (pad "1" smd rect (at 0 0)(size 1 1)(layers "F.Cu")(net 1 "GND"))))"#; + let spec = serde_json::json!({"hot":{"U1":{"watts":0.3,"tabNet":"GND"}}, + "clearance":0.2,"viaPower":[0.6,0.3], + "pours":[{"net":"GND","layers":["F.Cu","B.Cu"],"polygon":[[5,5],[15,5],[15,15],[5,15]]}]}); + let b = aiflow_board::Board::from_text("fixture", text.into()).unwrap(); + let (base, _) = super::plan(&spec, &b, &[]); + let (x,y) = base[0].planned[0]; + for layer in ["In1.Cu", "In2.Cu"] { + let mut blocked = text[..text.len()-1].to_string(); + blocked.push_str(&format!("(segment (start {} {})(end {} {})(width 0.25)(layer \"{layer}\")(net 2)))",x-2.0,y,x+2.0,y)); + let b = aiflow_board::Board::from_text("inner-track", blocked).unwrap(); + let (parts, _) = super::plan(&spec, &b, &[]); + for &p in &parts[0].planned { + assert!(super::seg_dist(p,(x-2.0,y),(x+2.0,y)) >= 0.3+0.125+0.2, + "through via must clear {layer} track: {p:?}"); + } + } + } + #[test] fn local_regions_reject_remote_disjoint_and_edge_candidates() { let pours = serde_json::json!([