app
AI Flow: Reverse Engineering
Public Made by Adomby adom
Photos of a PCB to a tagged KiCad copy: parts placed, copper and nets with their evidence, a schematic, a measured confidence score, bench tests and a video of the run.
main
Caleb
Publish 0.5.0
1984b40
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
//! What lies under print where a gap runs into it.
//!
//! Print is not read: each pixel under it takes its class from what can be seen around it. Where a gap between two conductors runs under a line or a block of print,
//! the pixels under the print are nearer to the copper on either side than to the gap's two ends, so the gap closed there and the two conductors became one island.
//! On the USB hub board's bottom the frame and labels of a printed table joined separate pours this way (baseline run, 2026-11-10); on its top a bracket of print
//! joined two tracks that pass under it side by side.
//!
//! Print lies on top of the copper pattern, so a gap that reaches print on one side and leaves it on the other, in line, is one gap that runs on underneath. Every
//! gap mouth at the print is walked straight on under it; where the walk comes out on laminate the walked pixels are gap. A mouth whose walk comes out on copper is
//! a gap that ended at the print by chance, and nothing is changed there. Only print counts (what the material reader calls silk, with its rim): a dome or a stretch
//! of glare on a track has gap on both its sides too, and must not cut the track.
use crate::raster::Mask;
/// The longest walk under print, frame pixels at 40 px/mm: 2 mm, the width of the label blocks on the hub board. Under wider print nothing is known.
const MAX_WALK: i64 = 80;
/// The pixels under `cover` (print and its rim) that a gap runs on through. `copper`: the class of every pixel after print took the class around it.
/// `hidden`: parts and holes, where not being copper is not a reading of laminate.
pub fn gaps_under(copper: &Mask, cover: &Mask, hidden: &Mask) -> Mask {
let (w, h) = (copper.w as i64, copper.h as i64);
let at = |x: i64, y: i64| (y * w + x) as usize;
let inside = |x: i64, y: i64| x >= 0 && y >= 0 && x < w && y < h;
let laminate = |i: usize| !copper.d[i] && !hidden.d[i] && !cover.d[i];
let mut out = Mask::new(copper.w, copper.h);
// a walk from (x, y) in steps of (dx, dy): the pixels crossed, if it stays under print and comes out on seen laminate
let walk = |x: i64, y: i64, dx: i64, dy: i64| -> Option<Vec<usize>> {
let mut run = vec![at(x, y)]; let (mut u, mut v) = (x, y);
for _ in 0..MAX_WALK { u += dx; v += dy; if !inside(u, v) { return None; } let j = at(u, v);
if hidden.d[j] { return None; } if cover.d[j] { run.push(j); if dx != 0 && dy != 0 { run.push(at(u - dx, v)); } continue; }
return if laminate(j) { Some(run) } else { None }; }
None
};
for y in 0..h { for x in 0..w { let i = at(x, y); if !cover.d[i] || !copper.d[i] || hidden.d[i] { continue; }
for (gx, gy) in [(-1i64, 0i64), (1, 0), (0, -1), (0, 1)] {
if !inside(x + gx, y + gy) || !laminate(at(x + gx, y + gy)) { continue; }
// the gap comes in from (gx, gy), so it runs on the other way: straight first, then to either side of straight for a gap that crosses at an angle
let (dx, dy) = (-gx, -gy); let (px, py) = (dy, dx);
for (sx, sy) in [(dx, dy), (dx + px, dy + py), (dx - px, dy - py)] {
if let Some(run) = walk(x, y, sx, sy) { for j in run { if cover.d[j] { out.d[j] = true; } } break; } }
} } }
out
}
#[cfg(test)]
mod tests {
use super::{gaps_under, Mask};
/// Two pours with a gap at x 96..104 and a band of print over y 40..80 under which the fill closed the gap.
fn face(w: usize, h: usize) -> (Mask, Mask) {
let (mut copper, mut cover) = (Mask::new(w, h), Mask::new(w, h));
for y in 0..h { for x in 0..w { let under = (40..80).contains(&y); copper.set(x, y, under || !(96..104).contains(&x)); cover.set(x, y, under); } }
(copper, cover)
}
#[test]
fn a_gap_that_enters_print_and_leaves_it_in_line_runs_on_under_it() {
let (w, h) = (200, 120); let (copper, cover) = face(w, h);
let g = gaps_under(&copper, &cover, &Mask::new(w, h));
for y in 40..80 { for x in 96..104 { assert!(g.at(x, y), "({x}, {y}) is gap"); } }
assert!(!g.at(60, 60) && !g.at(150, 60), "the pours under the print stay copper");
assert_eq!(g.count(), 40 * 8);
}
#[test]
fn a_gap_that_ends_at_print_with_copper_beyond_changes_nothing_and_neither_does_a_part() {
let (w, h) = (200, 120); let (mut copper, cover) = face(w, h);
for y in 80..h { for x in 96..104 { copper.set(x, y, true); } }
assert_eq!(gaps_under(&copper, &cover, &Mask::new(w, h)).count(), 0, "copper on the far side: the gap ended at the print");
let (copper, cover) = face(w, h); let mut hidden = Mask::new(w, h);
for y in 80..h { for x in 0..w { hidden.set(x, y, true); } }
assert_eq!(gaps_under(&copper, &cover, &hidden).count(), 0, "a part on the far side says nothing about laminate");
}
#[test]
fn a_gap_crossing_at_45_degrees_is_followed() {
let (w, h) = (200usize, 120usize); let (mut copper, mut cover) = (Mask::new(w, h), Mask::new(w, h));
// the gap runs down and to the right; print over y 50..62
for y in 0..h { for x in 0..w { let under = (50..62).contains(&y); let in_gap = (x as i64 - y as i64 - 40).abs() <= 4; copper.set(x, y, under || !in_gap); cover.set(x, y, under); } }
let g = gaps_under(&copper, &cover, &Mask::new(w, h));
assert!(g.at(96, 56) && g.at(95, 55) && g.at(97, 57), "the middle of the gap under the print");
assert!(!g.at(60, 56) && !g.at(130, 56));
}
}