app
Adom Fields
Public Made by Adomby adom
Adom Fields: where the current and the heat go on your board.
main
Colby Knox
adom-fields 0.10.6: grid covers the board (issue 27); one board thickness and solid-uncoupled (issue 28); decoupling co…
cb236bf
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
//! Pour continuity (issue 15): for each poured net and layer, how many separate pieces the fill broke into, whether the
//! net's vias join them through the other layers, and for a split pour the narrowest gap between its main piece and
//! the next one with the part (or the other net's copper) sitting in that gap, as a move the caller can make.
use crate::board::{Board, LAYERS};
use crate::fields::{Fields, Issue, Spec};
use crate::flow::is_ground;
use serde_json::{json, Value};
fn find(p: &mut [usize], mut a: usize) -> usize { while p[a] != a { p[a] = p[p[a]]; a = p[a]; } a }
fn r2(x: f64) -> f64 { (x * 100.0).round() / 100.0 }
pub fn review(f: &Fields, b: &Board, spec: &Spec, issues: &mut Vec<Issue>) -> Value {
let (w, h, d) = (f.w, f.h, f.d); let n = w * h; let nl = LAYERS.len();
let cell = |x: f64, y: f64| -> Option<usize> { crate::fields::cell_at(x, y, f.x0, f.y0, d, w, h).map(|(r, c)| r * w + c) };
let at = |i: usize| [f.x0 + ((i % w) as f64 + 0.5) * d, f.y0 + ((i / w) as f64 + 0.5) * d];
// a piece smaller than this is a sliver of fill, not a pour fragment worth a move
let min_cells = ((0.5 / (d * d)).ceil() as usize).max(2);
let mut nets: Vec<String> = b.polys.iter().map(|p| p.net.clone()).filter(|x| !x.is_empty()).collect(); nets.sort(); nets.dedup();
let mut out = Vec::new();
for net in &nets {
let Some(id) = f.net_names.iter().position(|x| x == net) else { continue }; let id = id as u16;
// the net's copper per layer, 4-connected pieces
let mut lab = vec![u32::MAX; nl * n]; let mut size: Vec<usize> = Vec::new(); let mut layer_of: Vec<usize> = Vec::new();
for k in 0..nl { for s in 0..n {
if f.net_ids[k][s] != id || lab[k * n + s] != u32::MAX { continue; }
let c = size.len() as u32; lab[k * n + s] = c; let mut st = vec![s]; let mut cnt = 0;
while let Some(i) = st.pop() { cnt += 1; let (r, cc) = (i / w, i % w);
for (ok, j) in [(cc > 0, i.wrapping_sub(1)), (cc + 1 < w, i + 1), (r > 0, i.wrapping_sub(w)), (r + 1 < h, i + w)] { if ok && f.net_ids[k][j] == id && lab[k * n + j] == u32::MAX { lab[k * n + j] = c; st.push(j); } } }
size.push(cnt); layer_of.push(k);
} }
// the pieces across the layers: a via or plated hole of the net joins every layer it lands copper on (the drill
// void is no copper, so its ring of cells is searched)
let mut par: Vec<usize> = (0..size.len()).collect();
for v in b.vias.iter().chain(b.plated_holes.iter()).filter(|v| v.net == *net) {
let Some(i) = cell(v.x, v.y) else { continue }; let rad = ((v.size / 2.0 / d).ceil() as i64).max(1); let (r0, c0) = ((i / w) as i64, (i % w) as i64);
let mut hit: Vec<usize> = Vec::new();
for k in 0..nl { 'k: for dr in -rad..=rad { for dc in -rad..=rad { let (r, c) = (r0 + dr, c0 + dc); if r < 0 || c < 0 || r >= h as i64 || c >= w as i64 { continue; } let l = lab[k * n + r as usize * w + c as usize]; if l != u32::MAX { hit.push(l as usize); break 'k; } } } }
for p in hit.windows(2) { let (a, z) = (find(&mut par, p[0]), find(&mut par, p[1])); if a != z { par[a] = z; } }
}
let important = spec.loads.contains_key(net) || is_ground(net);
for k in 0..nl {
// the pieces this layer's pour polygons landed in
let mut pieces: Vec<usize> = Vec::new();
for p in b.polys.iter().filter(|p| p.net == *net && p.layer == LAYERS[k]) {
for &(x, y) in p.pts.iter().step_by((p.pts.len() / 16).max(1)) { let Some(i) = cell(x, y) else { continue }; let (r0, c0) = ((i / w) as i64, (i % w) as i64);
'v: for dr in -1..=1i64 { for dc in -1..=1i64 { let (r, c) = (r0 + dr, c0 + dc); if r < 0 || c < 0 || r >= h as i64 || c >= w as i64 { continue; } let l = lab[k * n + r as usize * w + c as usize]; if l != u32::MAX { pieces.push(l as usize); break 'v; } } } }
}
pieces.sort(); pieces.dedup(); pieces.retain(|&p| size[p] >= min_cells);
if pieces.is_empty() { continue; }
pieces.sort_by(|a, z| size[*z].cmp(&size[*a]));
let main = pieces[0];
let mut rec = json!({"net": net, "layer": LAYERS[k], "pieces": pieces.len(), "areasMm2": pieces.iter().map(|&p| r2(size[p] as f64 * d * d)).collect::<Vec<_>>()});
if pieces.len() > 1 {
// the gap from the main piece to each other piece: one breadth-first sweep from the main piece over the
// layer, carrying where each cell was reached from
let mut src = vec![usize::MAX; n]; let mut q = std::collections::VecDeque::new();
for i in 0..n { if lab[k * n + i] == main as u32 { src[i] = i; q.push_back(i); } }
while let Some(i) = q.pop_front() { let (r, c) = ((i / w) as i64, (i % w) as i64);
for dr in -1..=1i64 { for dc in -1..=1i64 { let (rr, cc) = (r + dr, c + dc); if rr < 0 || cc < 0 || rr >= h as i64 || cc >= w as i64 { continue; } let j = rr as usize * w + cc as usize; if src[j] == usize::MAX { src[j] = src[i]; q.push_back(j); } } } }
let main_root = find(&mut par, main);
let mut others = Vec::new();
for &p in &pieces[1..] {
let (mut best, mut pair) = (f64::MAX, (0, 0));
for i in 0..n { if lab[k * n + i] == p as u32 && src[i] != usize::MAX { let (a, z) = (at(src[i]), at(i)); let g = (a[0] - z[0]).hypot(a[1] - z[1]); if g < best { best = g; pair = (src[i], i); } } }
let joined = find(&mut par, p) == main_root;
others.push((p, best, pair, joined));
}
others.sort_by(|a, z| a.1.partial_cmp(&z.1).unwrap());
let isolated = others.iter().filter(|o| !o.3).count();
rec["joinedThroughVias"] = json!(others.len() - isolated); rec["isolated"] = json!(isolated);
let (p, gap, (ia, ib), joined) = others[0];
let (a, z) = (at(ia), at(ib)); let m = [(a[0] + z[0]) / 2.0, (a[1] + z[1]) / 2.0];
// what sits in the gap: another net's pad on this layer near its middle, else the other net's copper across it
let on_layer = |pl: &[String]| pl.iter().any(|l| l == LAYERS[k] || l == "*.Cu");
let pad = b.pads.iter().filter(|q| q.net != *net && on_layer(&q.layers)).map(|q| (q, (q.x - m[0]).hypot(q.y - m[1]))).filter(|(_, dd)| *dd <= gap / 2.0 + 1.5).min_by(|x, y| x.1.partial_cmp(&y.1).unwrap()).map(|(q, _)| q.clone());
let mut across: std::collections::BTreeMap<u16, usize> = Default::default();
let steps = ((gap / d).ceil() as usize).max(1) * 2;
for s in 0..=steps { let t = s as f64 / steps as f64; if let Some(i) = cell(a[0] + (z[0] - a[0]) * t, a[1] + (z[1] - a[1]) * t) { let o = f.net_ids[k][i]; if o != id && o != u16::MAX { *across.entry(o).or_default() += 1; } } }
let blocking_net = across.iter().max_by_key(|x| x.1).map(|(o, _)| f.net_names[*o as usize].clone());
let gap_info = json!({"from": [r2(a[0]), r2(a[1])], "to": [r2(z[0]), r2(z[1])], "gapMm": r2(gap), "pieceAreaMm2": r2(size[p] as f64 * d * d), "joinedThroughVias": joined, "blockingPad": pad.as_ref().map(|q| format!("{}.{}", q.reference, q.pad)), "blockingNet": pad.as_ref().map(|q| q.net.clone()).or(blocking_net.clone())});
rec["nearestGap"] = gap_info.clone();
if important {
let sev = if isolated > 0 { "medium" } else { "low" };
let how = if joined { "joined only through the net's vias on other layers" } else { "not joined to the main piece on any layer" };
let (fix, action) = match &pad {
Some(q) => (format!("Rotate {} 90° or move it about {:.1} mm so its {} pad ({}) leaves the gap and {}'s pour on {} closes; re-pour and re-run.", q.reference, gap + 0.3, q.pad, q.net, net, LAYERS[k]),
json!({"type": "move", "reference": q.reference, "pad": q.pad, "orRotateDeg": 90, "byMm": r2(gap + 0.3), "net": net, "layer": LAYERS[k], "gap": gap_info, "why": "join a split pour"})),
None => (format!("Reroute {} out of the {:.1} mm gap at ({:.1}, {:.1}) on {} so {}'s pour closes; re-pour and re-run.", blocking_net.clone().unwrap_or_else(|| "the copper".into()), gap, m[0], m[1], LAYERS[k], net),
json!({"type": "reroute", "net": blocking_net, "at": [r2(m[0]), r2(m[1])], "layer": LAYERS[k], "gap": gap_info, "why": "join a split pour"})),
};
issues.push(Issue { severity: sev.into(), kind: "pour-fragmented".into(), what: format!("{net}: its pour on {} is in {} pieces ({} joined through vias, {} isolated); the nearest one ({:.1} mm², {how}) is {:.1} mm from the main piece", LAYERS[k], pieces.len(), others.len() - isolated, isolated, size[p] as f64 * d * d, gap), at: Some([r2(m[0]), r2(m[1])]), layer: Some(LAYERS[k].into()), net: Some(net.clone()), reference: pad.as_ref().map(|q| q.reference.clone()), fix, action: Some(action) });
}
}
out.push(rec);
}
}
json!(out)
}