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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
//! Copper masks on a fine grid, pooled to the solve grid as copper fractions.
use crate::board::{Board, Poly, LAYERS};
use std::collections::HashMap;
pub struct Raster { pub w: usize, pub h: usize, pub res: f64, pub x0: f64, pub y0: f64, pub masks: HashMap<(String, String), Vec<u8>> }
impl Raster {
pub fn new(b: &Board, res: f64) -> Raster { Raster::pooled(b, res, 1) }
/// A fine grid that pools `pool` pixels into each solve cell: whole cells, rounded up so the grid covers the whole
/// outline and a pad on the far edge has copper in it (issue 27). The 1e-6 keeps a board that is an exact number of
/// cells from gaining a column to floating-point noise.
pub fn pooled(b: &Board, res: f64, pool: usize) -> Raster {
let px = |mm: f64| ((mm / (res * pool as f64) - 1e-6).ceil() as usize).max(1) * pool;
Raster { w: px(b.width()), h: px(b.height()), res, x0: b.x0, y0: b.y0, masks: HashMap::new() }
}
fn mask(&mut self, net: &str, layer: &str) -> &mut Vec<u8> {
let n = self.w * self.h;
self.masks.entry((net.to_string(), layer.to_string())).or_insert_with(|| vec![0u8; n])
}
fn px(&self, x: f64, y: f64) -> (f64, f64) { ((x - self.x0) / self.res, (y - self.y0) / self.res) }
pub fn fill_poly(&mut self, net: &str, layer: &str, pts: &[(f64, f64)]) {
let (w, h) = (self.w, self.h);
let p: Vec<(f64, f64)> = pts.iter().map(|(x, y)| self.px(*x, *y)).collect();
let m = self.mask(net, layer);
let ymin = p.iter().map(|q| q.1).fold(f64::MAX, f64::min).floor().max(0.0) as usize;
let ymax = p.iter().map(|q| q.1).fold(f64::MIN, f64::max).ceil().min(h as f64 - 1.0) as usize;
let mut xs: Vec<f64> = Vec::new();
for row in ymin..=ymax {
let yc = row as f64 + 0.5;
xs.clear();
for i in 0..p.len() {
let (a, b) = (p[i], p[(i + 1) % p.len()]);
if (a.1 <= yc && b.1 > yc) || (b.1 <= yc && a.1 > yc) { xs.push(a.0 + (yc - a.1) / (b.1 - a.1) * (b.0 - a.0)); }
}
xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
for pair in xs.chunks(2) {
if pair.len() < 2 { break; }
let (xa, xb) = ((pair[0].round().max(0.0)) as usize, (pair[1].round().min(w as f64)) as usize);
for col in xa..xb { m[row * w + col] = 1; }
}
}
}
pub fn disc(&mut self, net: &str, layer: &str, x: f64, y: f64, r: f64) {
let (cx, cy) = self.px(x, y); let rp = r / self.res; let (w, h) = (self.w, self.h);
let m = self.mask(net, layer);
let (r0, r1) = (((cy - rp).floor().max(0.0)) as usize, ((cy + rp).ceil().min(h as f64 - 1.0)) as usize);
let (c0, c1) = (((cx - rp).floor().max(0.0)) as usize, ((cx + rp).ceil().min(w as f64 - 1.0)) as usize);
for row in r0..=r1 { for col in c0..=c1 { let (dx, dy) = (col as f64 + 0.5 - cx, row as f64 + 0.5 - cy); if dx * dx + dy * dy <= rp * rp { m[row * w + col] = 1; } } }
}
pub fn thick_line(&mut self, net: &str, layer: &str, a: (f64, f64), b: (f64, f64), wmm: f64) {
let (ax, ay) = self.px(a.0, a.1); let (bx, by) = self.px(b.0, b.1); let r = wmm / 2.0 / self.res; let (w, h) = (self.w, self.h);
let m = self.mask(net, layer);
let (r0, r1) = (((ay.min(by) - r).floor().max(0.0)) as usize, ((ay.max(by) + r).ceil().min(h as f64 - 1.0)) as usize);
let (c0, c1) = (((ax.min(bx) - r).floor().max(0.0)) as usize, ((ax.max(bx) + r).ceil().min(w as f64 - 1.0)) as usize);
let (dx, dy) = (bx - ax, by - ay); let len2 = dx * dx + dy * dy;
for row in r0..=r1 { for col in c0..=c1 {
let (px, py) = (col as f64 + 0.5, row as f64 + 0.5);
let t = if len2 > 0.0 { ((px - ax) * dx + (py - ay) * dy) / len2 } else { 0.0 }.clamp(0.0, 1.0);
let (qx, qy) = (ax + t * dx, ay + t * dy);
if (px - qx).powi(2) + (py - qy).powi(2) <= r * r { m[row * w + col] = 1; }
} }
}
/// Draw everything on the board: filled polygons, tracks, pads, via annular rings.
pub fn draw_board(&mut self, b: &Board) {
let polys: Vec<Poly> = b.polys.iter().chain(b.pad_polys.iter()).cloned().collect();
for p in &polys { self.fill_poly(&p.net, &p.layer, &p.pts); }
for t in &b.tracks { let (net, layer, a, bb, w) = (t.net.clone(), t.layer.clone(), t.a, t.b, t.w); self.thick_line(&net, &layer, a, bb, w); }
// a via's pad ring on the copper the board has, never on an empty slot (PR 5)
let cu: Vec<&str> = if b.copper.is_empty() { LAYERS.to_vec() } else { b.copper.iter().map(String::as_str).collect() };
for v in &b.vias { for &l in &cu { let (net, x, y, s) = (v.net.clone(), v.x, v.y, v.size); self.disc(&net, l, x, y, s / 2.0); } }
self.draw_fills(b);
self.fill_pours(b);
// drill voids: no copper in a drilled hole on any net or layer, even where a track or pour crosses it (PR 9)
let key = "\u{0}drills";
for v in b.vias.iter().chain(b.plated_holes.iter()) { let (a, z) = crate::barrel::axis(v); self.thick_line(key, key, a, z, v.drill); }
if let Some(holes) = self.masks.remove(&(key.to_string(), key.to_string())) { for m in self.masks.values_mut() { for (c, h) in m.iter_mut().zip(&holes) { if *h != 0 { *c = 0; } } } }
}
/// Pour islands the file carries already filled (Fusion): each outline minus its holes.
fn draw_fills(&mut self, b: &Board) {
let (w, h) = (self.w, self.h);
for f in &b.fills {
let (so, sh) = ("\u{0}fill", "\u{0}hole");
self.masks.remove(&(so.to_string(), f.layer.clone())); self.masks.remove(&(sh.to_string(), f.layer.clone()));
self.fill_poly(so, &f.layer, &f.outer);
for hole in &f.holes { self.fill_poly(sh, &f.layer, hole); }
let outer = self.masks.remove(&(so.to_string(), f.layer.clone())).unwrap_or_else(|| vec![0u8; w * h]);
let holes = self.masks.remove(&(sh.to_string(), f.layer.clone())).unwrap_or_else(|| vec![0u8; w * h]);
let m = self.mask(&f.net, &f.layer);
for i in 0..w * h { if outer[i] != 0 && holes[i] == 0 { m[i] = 1; } }
}
}
/// Fill the pours whose fill the file does not carry (Fusion/EAGLE polygons), the way Fusion
/// computes it: the outline, inside the board, minus every other net's copper on that layer grown
/// by the pour's isolate distance, keeping only the islands that touch the pour's own copper
/// (Fusion's default: orphans off). Higher-priority pours (lower rank) are filled first and count
/// as other-net copper for the ones after them.
fn fill_pours(&mut self, b: &Board) {
let (w, h) = (self.w, self.h);
let mut pours: Vec<&crate::board::Pour> = b.pours.iter().collect();
pours.sort_by_key(|p| p.rank);
for p in pours {
// the outline alone, in a scratch mask
let scratch = "\u{0}pour";
self.masks.remove(&(scratch.to_string(), p.layer.clone()));
self.fill_poly(scratch, &p.layer, &p.pts);
let Some(mut fill) = self.masks.remove(&(scratch.to_string(), p.layer.clone())) else { continue };
// other nets' copper on this layer, grown by the isolate distance
let grow = (p.isolate / self.res).ceil() as i64;
let mut blocked = vec![0u8; w * h];
for ((net, layer), m) in &self.masks {
if layer != &p.layer || net == &p.net { continue; }
for i in 0..w * h { if m[i] != 0 { blocked[i] = 1; } }
}
let blocked = dilate(&blocked, w, h, grow);
for i in 0..w * h { if blocked[i] != 0 { fill[i] = 0; } }
// keep the islands that touch the net's own copper
let own = self.masks.get(&(p.net.clone(), p.layer.clone())).cloned().unwrap_or_else(|| vec![0u8; w * h]);
let mut keep = vec![0u8; w * h];
let mut stack: Vec<usize> = (0..w * h).filter(|i| fill[*i] != 0 && own[*i] != 0).collect();
for i in &stack { keep[*i] = 1; }
while let Some(i) = stack.pop() {
let (r, c) = (i / w, i % w);
let mut nb = Vec::with_capacity(4);
if r > 0 { nb.push(i - w); } if r + 1 < h { nb.push(i + w); } if c > 0 { nb.push(i - 1); } if c + 1 < w { nb.push(i + 1); }
for j in nb { if fill[j] != 0 && keep[j] == 0 { keep[j] = 1; stack.push(j); } }
}
let m = self.mask(&p.net, &p.layer);
for i in 0..w * h { if keep[i] != 0 { m[i] = 1; } }
}
}
/// The copper fraction of one net on one layer per solve cell (pool x pool fine pixels each).
pub fn fraction(&self, net: &str, layer: &str, pool: usize) -> Grid {
let (gw, gh) = (self.w / pool, self.h / pool);
let mut g = Grid::new(gw, gh);
if let Some(m) = self.masks.get(&(net.to_string(), layer.to_string())) {
for r in 0..gh { for c in 0..gw {
let mut s = 0u32;
for i in 0..pool { for j in 0..pool { s += m[(r * pool + i) * self.w + c * pool + j] as u32; } }
g.v[r * gw + c] = s as f32 / (pool * pool) as f32;
} }
}
g
}
pub fn nets_on(&self, layer: &str) -> Vec<String> { let mut v: Vec<String> = self.masks.keys().filter(|(_, l)| l == layer).map(|(n, _)| n.clone()).collect(); v.sort(); v.dedup(); v }
}
#[derive(Clone, Debug)]
pub struct Grid { pub w: usize, pub h: usize, pub v: Vec<f32> }
impl Grid {
pub fn new(w: usize, h: usize) -> Grid { Grid { w, h, v: vec![0.0; w * h] } }
pub fn at(&self, r: usize, c: usize) -> f32 { self.v[r * self.w + c] }
pub fn max_with(&mut self, o: &Grid) { for (a, b) in self.v.iter_mut().zip(o.v.iter()) { if *b > *a { *a = *b; } } }
pub fn sum(&self) -> f64 { self.v.iter().map(|x| *x as f64).sum() }
pub fn max(&self) -> f32 { self.v.iter().cloned().fold(0.0, f32::max) }
}
/// Grow a mask by r pixels (a disc), separably approximated by a square of the same radius then
/// trimmed to the disc.
fn dilate(m: &[u8], w: usize, h: usize, r: i64) -> Vec<u8> {
if r <= 0 { return m.to_vec(); }
let mut out = vec![0u8; w * h];
let offs: Vec<(i64, i64)> = (-r..=r).flat_map(|dy| (-r..=r).map(move |dx| (dx, dy))).filter(|(dx, dy)| dx * dx + dy * dy <= r * r).collect();
for i in 0..w * h {
if m[i] == 0 { continue; }
let (r0, c0) = ((i / w) as i64, (i % w) as i64);
for (dx, dy) in &offs {
let (rr, cc) = (r0 + dy, c0 + dx);
if rr >= 0 && cc >= 0 && (rr as usize) < h && (cc as usize) < w { out[rr as usize * w + cc as usize] = 1; }
}
}
out
}
#[cfg(test)] mod drill_tests {
use super::*;
// issue 27: whole solve cells, rounded up so they cover the outline; an exact fit gains nothing
#[test] fn pooled_grid_covers_the_outline() {
let g = |x1: f64, y1: f64| { let r = Raster::pooled(&Board { x1, y1, ..Board::default() }, 0.1, 2); (r.w, r.h) };
assert_eq!(g(49.53, 20.0), (496, 200)); assert_eq!(g(49.41, 20.03), (496, 202)); assert_eq!(g(50.0, 28.0), (500, 280)); assert_eq!(g(0.01, 0.01), (2, 2));
// the same when a rounding error leaves the width a hair over a whole cell
assert_eq!(g(16.200000000000003, 60.1 - 40.1), (162, 200));
}
// PR 9: a drill is a void on every layer even where a pour crosses it; a slot is void along its own axis
#[test] fn drill_voids() {
let mut b = Board { x1: 4.0, y1: 4.0, layer_names: vec!["F.Cu".into(), "B.Cu".into()], copper: vec!["F.Cu".into(), "B.Cu".into()], ..Board::default() };
for l in ["F.Cu", "B.Cu"] { b.polys.push(Poly { net: "N".into(), layer: l.into(), pts: vec![(0., 0.), (4., 0.), (4., 4.), (0., 4.)] }); }
b.vias.push(crate::board::Via { net: "N".into(), x: 2.0, y: 2.0, size: 1.4, drill: 0.8, drill_long: None, drill_angle: 0.0 });
let mut r = Raster::new(&b, 0.025); r.draw_board(&b);
for l in ["F.Cu", "B.Cu"] { let g = r.fraction("N", l, 2); assert_eq!(g.at(40, 40), 0.0); assert_eq!(g.at(40, 50), 1.0); assert!((16.0 - g.sum() * 0.05 * 0.05 - std::f64::consts::PI * 0.16).abs() < 0.025); }
}
#[test] fn rotated_slot() {
let mut b = Board { x1: 4.0, y1: 4.0, layer_names: vec!["F.Cu".into()], copper: vec!["F.Cu".into()], ..Board::default() };
b.polys.push(Poly { net: "N".into(), layer: "F.Cu".into(), pts: vec![(0., 0.), (4., 0.), (4., 4.), (0., 4.)] });
b.plated_holes.push(crate::board::Via { net: "N".into(), x: 2., y: 2., size: 2., drill: 0.4, drill_long: Some(1.4), drill_angle: 90. });
let mut r = Raster::new(&b, 0.025); r.draw_board(&b); let g = r.fraction("N", "F.Cu", 2);
assert_eq!(g.at(30, 40), 0.0); assert_eq!(g.at(40, 30), 1.0);
}
}