use super::{copper_and_nets, mm, oriented_outline, xy, BOARD_SOLID_PREFIX, SUBSTRATE_SOLID, VIAS_SOLID};
use brep_ecad_core::board::{self, Board, CopperRef, Shape};
use brep_ecad_core::Netlist;
use brep_kernel::{
boolean_operation, extrude_profile_brep, make_arc, make_cylinder_brep, make_line, revolve_profile_brep,
BooleanOperation, BooleanOptions, BrepSolid, NurbsCurve, Vec3,
};
pub(crate) const SUBSTRATE_RGB: [u8; 3] = [0x1c, 0x5e, 0x38];
pub(crate) const COPPER_RGB: [u8; 3] = [0xc0, 0x7b, 0x38];
#[derive(Default)]
pub(crate) struct BoardStepSolids {
pub bodies: Vec<(String, BrepSolid)>,
pub colors: Vec<(String, [u8; 3])>,
pub skipped: Vec<String>,
pub notices: Vec<String>,
}
#[derive(Clone, Copy)]
struct Hole {
at: [f64; 2],
diameter: f64,
}
fn point(p: [f64; 2], z: f64) -> Vec3 {
Vec3::new(p[0], p[1], z)
}
fn polygon_curves(loop_xy: &[[f64; 2]], z: f64) -> Result<Vec<NurbsCurve>, String> {
(0..loop_xy.len())
.map(|i| make_line(point(loop_xy[i], z), point(loop_xy[(i + 1) % loop_xy.len()], z)))
.collect()
}
fn half_arc(centre: [f64; 2], radius: f64, start: f64, z: f64) -> Result<NurbsCurve, String> {
make_arc(
point(centre, z),
Vec3::new(1., 0., 0.),
Vec3::new(0., 1., 0.),
radius,
start,
start + std::f64::consts::PI,
)
}
fn shape_curves(shape: &Shape, z: f64) -> Result<Vec<NurbsCurve>, String> {
match *shape {
Shape::Rect { min, max } => {
let (a, b) = (xy(min), xy(max));
let (x0, x1) = (a[0].min(b[0]), a[0].max(b[0]));
let (y0, y1) = (a[1].min(b[1]), a[1].max(b[1]));
if x1 - x0 <= 1e-6 || y1 - y0 <= 1e-6 {
return Err("degenerate rectangle".into());
}
polygon_curves(&[[x0, y0], [x1, y0], [x1, y1], [x0, y1]], z)
}
Shape::Capsule { a, b, radius } => {
let (a, b, r) = (xy(a), xy(b), mm(radius));
if r <= 1e-6 {
return Err("zero-radius capsule".into());
}
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
if (dx * dx + dy * dy).sqrt() <= 1e-6 {
return Ok(vec![half_arc(a, r, 0., z)?, half_arc(a, r, std::f64::consts::PI, z)?]);
}
let base = dy.atan2(dx) - std::f64::consts::FRAC_PI_2;
let at = |c: [f64; 2], t: f64| [c[0] + r * t.cos(), c[1] + r * t.sin()];
let (pi, far) = (std::f64::consts::PI, base + std::f64::consts::PI);
Ok(vec![
half_arc(b, r, base, z)?,
make_line(point(at(b, base + pi), z), point(at(a, far), z))?,
half_arc(a, r, far, z)?,
make_line(point(at(a, far + pi), z), point(at(b, base), z))?,
])
}
}
}
fn prism(curves: &[NurbsCurve], lo: f64, hi: f64) -> Result<BrepSolid, String> {
extrude_profile_brep(curves, Vec3::new(0., 0., 1.), hi - lo).map_err(String::from)
}
fn ring(centre: [f64; 2], r_in: f64, r_out: f64, lo: f64, hi: f64) -> Result<BrepSolid, String> {
if !(r_in > 1e-6 && r_out > r_in + 1e-6 && hi > lo) {
return Err(format!("degenerate ring r {r_in}..{r_out}"));
}
let at = |r: f64, z: f64| Vec3::new(centre[0] + r, centre[1], z);
let profile = [
make_line(at(r_in, lo), at(r_out, lo))?,
make_line(at(r_out, lo), at(r_out, hi))?,
make_line(at(r_out, hi), at(r_in, hi))?,
make_line(at(r_in, hi), at(r_in, lo))?,
];
revolve_profile_brep(&profile, point(centre, lo), Vec3::new(0., 0., 1.), std::f64::consts::TAU)
}
fn drilled(shape: &Shape, hole: Hole, lo: f64, hi: f64) -> Result<BrepSolid, String> {
if let Shape::Capsule { a, b, radius } = *shape {
let centred = (xy(a)[0] - hole.at[0]).hypot(xy(a)[1] - hole.at[1]) <= 1e-6;
if a == b && centred {
return ring(hole.at, hole.diameter / 2., mm(radius), lo, hi);
}
}
let body = prism(&shape_curves(shape, lo)?, lo, hi)?;
drill(body, &[hole], lo, hi)
}
fn cutter(hole: &Hole, lo: f64, hi: f64) -> Result<BrepSolid, String> {
let margin = 1.0;
make_cylinder_brep(
point(hole.at, lo - margin),
Vec3::new(0., 0., 1.),
hole.diameter / 2.,
hi - lo + 2. * margin,
)
}
fn drill(body: BrepSolid, holes: &[Hole], lo: f64, hi: f64) -> Result<BrepSolid, String> {
if holes.is_empty() {
return Ok(body);
}
let mut operands = Vec::with_capacity(holes.len() + 1);
operands.push(body.clone());
for hole in holes {
operands.push(cutter(hole, lo, hi)?);
}
if let Ok(drilled) = brep_kernel::boolean_operation_nary(&operands, BooleanOperation::Subtract) {
return Ok(drilled);
}
let options = BooleanOptions::default();
let mut body = body;
for hole in holes {
body = boolean_operation(&body, &cutter(hole, lo, hi)?, BooleanOperation::Subtract, &options)
.map_err(|refusal| format!("hole at ({:.3}, {:.3}): {refusal}", hole.at[0], hole.at[1]))?;
}
Ok(body)
}
fn fill_solid(piece: &board::FillPiece, lo: f64, hi: f64) -> Result<BrepSolid, String> {
let ring = |points: &[brep_ecad_core::Point], outward: bool| {
let mut ring: Vec<[f64; 2]> = points.iter().copied().map(xy).collect();
if (super::signed_area2(&ring) > 0.) != outward {
ring.reverse();
}
ring
};
let body = prism(&polygon_curves(&ring(&piece.outer, true), lo)?, lo, hi)?;
if piece.holes.is_empty() {
return Ok(body);
}
let margin = 1.0;
let cutters = piece
.holes
.iter()
.map(|hole| {
let curves = polygon_curves(&ring(hole, true), lo - margin)?;
prism(&curves, lo - margin, hi + margin)
})
.collect::<Result<Vec<_>, String>>()?;
let mut operands = Vec::with_capacity(cutters.len() + 1);
operands.push(body.clone());
operands.extend(cutters.iter().cloned());
if let Ok(cut) = brep_kernel::boolean_operation_nary(&operands, BooleanOperation::Subtract) {
return Ok(cut);
}
let options = BooleanOptions::default();
let mut body = body;
for (k, cutter) in cutters.iter().enumerate() {
body = boolean_operation(&body, cutter, BooleanOperation::Subtract, &options)
.map_err(|refusal| format!("fill hole {k}: {refusal}"))?;
}
Ok(body)
}
fn item_hole(board: &Board, owner: &CopperRef) -> Option<Hole> {
match *owner {
CopperRef::Via(index) => board.vias.get(index).map(|via| Hole {
at: xy(via.at),
diameter: mm(via.drill),
}),
CopperRef::Pad { placement, pad } => {
let placement = board.placements.get(placement)?;
let pad = placement.footprint.pads.get(pad)?;
pad.drill.map(|drill| Hole {
at: xy(placement.transform(pad.at)),
diameter: mm(drill),
})
}
CopperRef::Track { .. } => None,
}
}
fn board_holes(board: &Board) -> Vec<Hole> {
let vias = (0..board.vias.len()).map(CopperRef::Via);
let pads = board.placements.iter().enumerate().flat_map(|(placement, p)| {
(0..p.footprint.pads.len()).map(move |pad| CopperRef::Pad { placement, pad })
});
vias.chain(pads)
.filter_map(|owner| item_hole(board, &owner))
.filter(|hole| hole.diameter > 1e-6)
.collect()
}
fn net_part(net: Option<&str>) -> String {
net.map(|net| net.replace('/', "_")).unwrap_or_else(|| "no-net".to_string())
}
pub(crate) fn board_step_solids(board: &Board, netlist: Option<&Netlist>) -> Option<BoardStepSolids> {
let outline = oriented_outline(board)?;
let mut out = BoardStepSolids::default();
let refilled;
let board = if board.zones.is_empty() {
board
} else {
let mut copy = board.clone();
let empty = Netlist { version: 1, nets: vec![], diagnostics: vec![] };
copy.fill_zones(netlist.unwrap_or(&empty));
if copy.zones != board.zones {
out.notices.push(
"STEP export: the zones' fill was out of date, so the file carries a fresh fill, as the fabrication \
export does. Fill zones (B) on the board to see the copper that was written."
.to_owned(),
);
}
refilled = copy;
&refilled
};
let holes = board_holes(board);
let (board_lo, board_hi) = (mm(-board::SUBSTRATE_THICKNESS), 0.);
let substrate = polygon_curves(&outline, board_lo)
.and_then(|curves| prism(&curves, board_lo, board_hi))
.and_then(|body| drill(body, &holes, board_lo, board_hi));
match substrate {
Ok(body) => {
out.bodies.push((SUBSTRATE_SOLID.to_string(), body));
out.colors.push((SUBSTRATE_SOLID.to_string(), SUBSTRATE_RGB));
}
Err(error) => out.skipped.push(format!("{SUBSTRATE_SOLID}: {error}")),
}
let (copper, nets) = copper_and_nets(board, netlist);
for layer in 0..board.layer_count.max(1) {
let (lo, hi) = super::copper_span(layer, board.layer_count);
let layer_name = format!("{BOARD_SOLID_PREFIX}{}", board::layer_name(layer, board.layer_count));
let mut count = 0usize;
for (item, net) in copper.iter().zip(&nets) {
if !(item.layers.0 <= layer && layer <= item.layers.1) {
continue;
}
count += 1;
let name = format!("{layer_name}/{}#{count}", net_part(net.as_deref()));
let solid = match item_hole(board, &item.owner) {
Some(hole) => drilled(&item.shape, hole, lo, hi),
None => shape_curves(&item.shape, lo).and_then(|curves| prism(&curves, lo, hi)),
};
match solid {
Ok(body) => {
out.colors.push((name.clone(), COPPER_RGB));
out.bodies.push((name, body));
}
Err(error) => out.skipped.push(format!("{name}: {error}")),
}
}
for zone in board.zones.iter().filter(|z| z.layer == layer) {
for piece in &zone.fill {
count += 1;
let name = format!("{layer_name}/{}#{count}", net_part(Some(&zone.net)));
match fill_solid(piece, lo, hi) {
Ok(body) => {
out.colors.push((name.clone(), COPPER_RGB));
out.bodies.push((name, body));
}
Err(error) => out.skipped.push(format!("{name}: {error}")),
}
}
}
}
for (index, via) in board.vias.iter().enumerate() {
let name = format!("{VIAS_SOLID}#{}", index + 1);
let inner = mm(via.drill) / 2.;
let outer = inner + mm(board::COPPER_THICKNESS);
let barrel = ring(xy(via.at), inner, outer, board_lo, board_hi);
match barrel {
Ok(body) => {
out.colors.push((name.clone(), COPPER_RGB));
out.bodies.push((name, body));
}
Err(error) => out.skipped.push(format!("{name}: {error}")),
}
}
Some(out)
}