use super::*;
use brep_ecad_core::board::{self, Board, CopperItem, Shape};
use brep_ecad_core::Netlist;
use brep_kernel::{DisplaySolidPayload, Mesh, Vec3};
use std::collections::BTreeMap;
pub const BOARD_SOLID_PREFIX: &str = "PCB_";
pub const SUBSTRATE_SOLID: &str = "PCB_Board";
pub const VIAS_SOLID: &str = "PCB_Vias";
const FACE_SEPARATOR: char = '/';
const NO_NET_FACE: &str = "(no net)";
const OPENING_FACE: &str = "Opening";
const LACQUER_FACE: &str = "Lacquer";
const SIDE_FACE: &str = "Side";
const CORNER_TURN: f64 = 0.5;
const EDGE_COLOR: [f32; 3] = [0xb8 as f32 / 255., 0xa8 as f32 / 255., 0x7a as f32 / 255.];
const SUBSTRATE_COLOR: [f32; 3] = [0x1c as f32 / 255., 0x5e as f32 / 255., 0x38 as f32 / 255.];
const COPPER_COLOR: [f32; 3] = [0xc0 as f32 / 255., 0x7b as f32 / 255., 0x38 as f32 / 255.];
const MASK_COLOR: [f32; 3] = [0x12 as f32 / 255., 0x44 as f32 / 255., 0x26 as f32 / 255.];
const SILK_COLOR: [f32; 3] = [0xec as f32 / 255., 0xec as f32 / 255., 0xe4 as f32 / 255.];
const MASK_OVER_COPPER: i32 = 20;
const SILK_THICKNESS: i32 = 15;
const ARC_FACETS: usize = 4;
fn xy(p: brep_ecad_core::Point) -> [f64; 2] {
[f64::from(p.x) / 1000., -f64::from(p.y) / 1000.]
}
fn mm(v: i32) -> f64 {
f64::from(v) / 1000.
}
#[derive(Default)]
struct Soup {
mesh: Mesh,
}
impl Soup {
fn vertex(&mut self, p: [f64; 3], n: [f64; 3]) -> u32 {
self.mesh.positions.extend(p);
self.mesh.normals.extend(n);
(self.mesh.positions.len() / 3 - 1) as u32
}
fn quad(&mut self, a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3], face: u32) {
let n = normal(a, b, c);
let [ia, ib, ic, id] = [a, b, c, d].map(|p| self.vertex(p, n));
self.mesh.indices.extend([ia, ib, ic, ia, ic, id]);
self.mesh.face_ids.extend([face, face]);
}
fn walls(&mut self, loop_xy: &[[f64; 2]], lo: f64, hi: f64, face: u32) {
for i in 0..loop_xy.len() {
let a = loop_xy[i];
let b = loop_xy[(i + 1) % loop_xy.len()];
self.quad(
[a[0], a[1], lo],
[b[0], b[1], lo],
[b[0], b[1], hi],
[a[0], a[1], hi],
face,
);
}
}
fn walls_open(&mut self, run: &[[f64; 2]], lo: f64, hi: f64, face: u32) {
for pair in run.windows(2) {
let (a, b) = (pair[0], pair[1]);
self.quad([a[0], a[1], lo], [b[0], b[1], lo], [b[0], b[1], hi], [a[0], a[1], hi], face);
}
}
fn cap(&mut self, loop_xy: &[[f64; 2]], triangles: &[[usize; 3]], z: f64, up: bool, face: u32) {
let n = if up { [0., 0., 1.] } else { [0., 0., -1.] };
let first = self.mesh.positions.len() as u32 / 3;
for p in loop_xy {
self.vertex([p[0], p[1], z], n);
}
for &[a, b, c] in triangles {
let (b, c) = if up { (b, c) } else { (c, b) };
self.mesh.indices.extend([a, b, c].map(|i| first + i as u32));
self.mesh.face_ids.push(face);
}
}
fn slab(&mut self, piece: &board::FillPiece, lo: f64, hi: f64, face: u32) {
let rings: Vec<&[brep_ecad_core::Point]> = std::iter::once(piece.outer.as_slice())
.chain(piece.holes.iter().map(Vec::as_slice))
.collect();
let triangles = board::zones::fill_triangles(&rings);
for (z, up) in [(hi, true), (lo, false)] {
let n = if up { [0., 0., 1.] } else { [0., 0., -1.] };
let mut shared: std::collections::HashMap<(u64, u64), u32> = std::collections::HashMap::new();
for &[a, b, c] in &triangles {
let ccw = (b.0 - a.0) * (c.1 - a.1) - (b.1 - a.1) * (c.0 - a.0) < 0.;
let (b, c) = if ccw == up { (b, c) } else { (c, b) };
let corners = [a, b, c].map(|(x, y)| {
*shared
.entry((x.to_bits(), y.to_bits()))
.or_insert_with(|| self.vertex([x / 1000., -y / 1000., z], n))
});
self.mesh.indices.extend(corners);
self.mesh.face_ids.push(face);
}
}
for (k, ring) in rings.iter().enumerate() {
let mut ring: Vec<[f64; 2]> = ring.iter().copied().map(xy).collect();
if (signed_area2(&ring) < 0.) == (k == 0) {
ring.reverse();
}
self.walls(&ring, lo, hi, face);
}
}
fn prism(&mut self, loop_xy: &[[f64; 2]], lo: f64, hi: f64, face: u32) {
let triangles = fan_or_ears(loop_xy);
self.cap(loop_xy, &triangles, lo, false, face);
self.cap(loop_xy, &triangles, hi, true, face);
self.walls(loop_xy, lo, hi, face);
}
}
fn normal(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> [f64; 3] {
let u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let n = [
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0],
];
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if len > 1e-12 {
[n[0] / len, n[1] / len, n[2] / len]
} else {
[0., 0., 1.]
}
}
fn signed_area2(loop_xy: &[[f64; 2]]) -> f64 {
let mut sum = 0.;
for i in 0..loop_xy.len() {
let a = loop_xy[i];
let b = loop_xy[(i + 1) % loop_xy.len()];
sum += a[0] * b[1] - b[0] * a[1];
}
sum
}
fn fan_or_ears(loop_xy: &[[f64; 2]]) -> Vec<[usize; 3]> {
let n = loop_xy.len();
if n < 3 {
return Vec::new();
}
let fan = || (1..n - 1).map(|i| [0, i, i + 1]).collect::<Vec<_>>();
let mut remaining: Vec<usize> = (0..n).collect();
let mut out = Vec::with_capacity(n - 2);
let convex = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
};
let inside = |a: [f64; 2], b: [f64; 2], c: [f64; 2], p: [f64; 2]| {
convex(a, b, p) >= 0. && convex(b, c, p) >= 0. && convex(c, a, p) >= 0.
};
while remaining.len() > 3 {
let before = remaining.len();
let mut i = 0;
while remaining.len() > 3 && i < remaining.len() {
let (ia, ib, ic) = (
remaining[(i + remaining.len() - 1) % remaining.len()],
remaining[i],
remaining[(i + 1) % remaining.len()],
);
let (a, b, c) = (loop_xy[ia], loop_xy[ib], loop_xy[ic]);
if convex(a, b, c) <= 0. {
i += 1;
continue;
}
if remaining
.iter()
.any(|&p| p != ia && p != ib && p != ic && inside(a, b, c, loop_xy[p]))
{
i += 1;
continue;
}
out.push([ia, ib, ic]);
remaining.remove(i);
}
if remaining.len() == before {
return fan();
}
}
out.push([remaining[0], remaining[1], remaining[2]]);
out
}
fn shape_loop(shape: &Shape) -> Vec<[f64; 2]> {
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]));
vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1]]
}
Shape::Capsule { a, b, radius } => {
let (a, b, r) = (xy(a), xy(b), mm(radius).max(1e-4));
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
let len = (dx * dx + dy * dy).sqrt();
if len <= 1e-9 {
return (0..ARC_FACETS * 2)
.map(|i| {
let t = std::f64::consts::TAU * i as f64 / (ARC_FACETS * 2) as f64;
[a[0] + r * t.cos(), a[1] + r * t.sin()]
})
.collect();
}
let base = dy.atan2(dx) - std::f64::consts::FRAC_PI_2;
let mut out = Vec::with_capacity(2 * (ARC_FACETS + 1));
for (centre, start) in [(b, base), (a, base + std::f64::consts::PI)] {
for i in 0..=ARC_FACETS {
let t = start + std::f64::consts::PI * i as f64 / ARC_FACETS as f64;
out.push([centre[0] + r * t.cos(), centre[1] + r * t.sin()]);
}
}
out
}
}
}
fn copper_span(layer: u8, layer_count: u8) -> (f64, f64) {
let plane = mm(board::layer_z(layer, layer_count));
let t = mm(board::COPPER_THICKNESS);
let last = layer_count.saturating_sub(1);
if layer == 0 {
(plane, plane + t)
} else if layer >= last {
(plane - t, plane)
} else {
(plane - t * 0.5, plane + t * 0.5)
}
}
fn side_frame(top: bool) -> (f64, f64) {
if top {
(0., 1.)
} else {
(mm(-board::SUBSTRATE_THICKNESS), -1.)
}
}
fn side_span(top: bool, from: i32, to: i32) -> (f64, f64) {
let (plane, out) = side_frame(top);
let (a, b) = (plane + out * mm(from), plane + out * mm(to));
(a.min(b), a.max(b))
}
const LACQUER_TOP: i32 = board::COPPER_THICKNESS + MASK_OVER_COPPER;
fn grown(shape: Shape, grow: i32) -> Shape {
match shape {
Shape::Rect { min, max } => Shape::Rect {
min: brep_ecad_core::Point::new(min.x - grow, min.y - grow),
max: brep_ecad_core::Point::new(max.x + grow, max.y + grow),
},
Shape::Capsule { a, b, radius } => Shape::Capsule {
a,
b,
radius: radius + grow,
},
}
}
fn oriented_outline(board: &Board) -> Option<Vec<[f64; 2]>> {
let mut outline: Vec<[f64; 2]> = board.outline.iter().copied().map(xy).collect();
outline.dedup();
if outline.len() > 1 && outline.first() == outline.last() {
outline.pop();
}
if outline.len() < 3 {
return None;
}
if signed_area2(&outline) < 0. {
outline.reverse();
}
Some(outline)
}
fn mask_solid(top: bool) -> String {
format!("{BOARD_SOLID_PREFIX}{}.Mask", if top { "F" } else { "B" })
}
fn silk_solid(top: bool) -> String {
format!("{BOARD_SOLID_PREFIX}{}.SilkS", if top { "F" } else { "B" })
}
const CURATED_RAILS: &[(&[&str], u32)] = &[
(&["GND", "VSS", "0V"], 0x3a3a40),
(&["VCC", "VDD", "+5V", "5V"], 0xcc1111),
(&["+3V3", "3V3", "+3.3V", "3.3V"], 0xff8000),
(&["+12V", "12V"], 0xe8d400),
(&["-12V"], 0x1b4fc8),
];
fn net_color(net: &str) -> [f32; 3] {
curated_rail_color(net)
.unwrap_or_else(|| crate::color::solid_color_srgb(net))
.map(|c| c as f32)
}
fn curated_rail_color(net: &str) -> Option<[f64; 3]> {
CURATED_RAILS.iter().find_map(|(spellings, hex)| {
spellings
.iter()
.any(|spelling| spelling.eq_ignore_ascii_case(net))
.then(|| crate::color::hex_to_srgb(*hex))
})
}
pub fn default_color(name: &str) -> Option<[f32; 3]> {
let rest = name.strip_prefix(BOARD_SOLID_PREFIX)?;
let Some((body, role)) = rest.split_once(FACE_SEPARATOR) else {
return Some(body_color(rest));
};
let color = face_color(body, role);
(color != body_color(body)).then_some(color)
}
fn body_color(body: &str) -> [f32; 3] {
if body == "Board" {
SUBSTRATE_COLOR
} else if body.ends_with(".Mask") {
MASK_COLOR
} else if body.ends_with(".SilkS") {
SILK_COLOR
} else {
COPPER_COLOR
}
}
fn face_color(body: &str, role: &str) -> [f32; 3] {
if body == "Board" {
if role.starts_with(SIDE_FACE) {
EDGE_COLOR
} else {
SUBSTRATE_COLOR
}
} else if body.ends_with(".Mask") {
if role == OPENING_FACE {
COPPER_COLOR
} else {
MASK_COLOR
}
} else if role == NO_NET_FACE {
COPPER_COLOR
} else {
net_color(role)
}
}
struct Body {
name: String,
color: [f32; 3],
payload: DisplaySolidPayload,
aux_from: usize,
}
fn body(name: String, color: [f32; 3], faces: Vec<String>, soup: Soup) -> Option<Body> {
if soup.mesh.indices.is_empty() {
return None;
}
Some(Body {
name,
color,
aux_from: usize::MAX,
payload: DisplaySolidPayload {
faces: faces
.into_iter()
.enumerate()
.map(|(i, name)| (i as u64, Some(name)))
.collect(),
mesh: soup.mesh,
edges: Vec::new(),
vertices: Vec::new(),
chord_tolerance: 0.,
},
})
}
fn substrate(board: &Board) -> Option<Body> {
let outline = oriented_outline(board)?;
let triangles = fan_or_ears(&outline);
let (lo, hi) = (mm(-board::SUBSTRATE_THICKNESS), 0.);
let n = outline.len();
let mut at = corners(&outline);
if at.is_empty() {
at.push(0);
}
let sides: Vec<Vec<[f64; 2]>> = (0..at.len())
.map(|k| {
let (from, to) = (at[k], at[(k + 1) % at.len()]);
let steps = (to + n - from - 1) % n + 1;
(0..=steps).map(|i| outline[(from + i) % n]).collect()
})
.collect();
let mut soup = Soup::default();
soup.cap(&outline, &triangles, hi, true, 0);
soup.cap(&outline, &triangles, lo, false, 1);
for (k, side) in sides.iter().enumerate() {
soup.walls_open(side, lo, hi, 2 + k as u32);
}
let face = |role: String| format!("{SUBSTRATE_SOLID}{FACE_SEPARATOR}{role}");
let side = |k: usize| face(format!("{SIDE_FACE} {}", k + 1));
let mut faces = vec![face("Top".into()), face("Bottom".into())];
faces.extend((0..sides.len()).map(side));
let mut body = body(SUBSTRATE_SOLID.to_string(), SUBSTRATE_COLOR, faces.clone(), soup)?;
let mut push = |name: String, points: Vec<Vec3>| {
let id = body.payload.edges.len() as u64;
body.payload.edges.push((id, Some(name), points));
};
for (k, run) in sides.iter().enumerate() {
for (cap, z) in [(&faces[0], hi), (&faces[1], lo)] {
let points = run.iter().map(|p| Vec3::new(p[0], p[1], z)).collect();
push(format!("{cap}|{}", side(k)), points);
}
}
if sides.len() > 1 {
for k in 0..sides.len() {
let p = outline[at[k]];
let before = (k + sides.len() - 1) % sides.len();
push(
format!("{}|{}", side(before), side(k)),
vec![Vec3::new(p[0], p[1], hi), Vec3::new(p[0], p[1], lo)],
);
}
}
Some(body)
}
fn corners(outline: &[[f64; 2]]) -> Vec<usize> {
let n = outline.len();
(0..n)
.filter(|&i| {
let (a, b, c) = (outline[(i + n - 1) % n], outline[i], outline[(i + 1) % n]);
let (u, v) = ([b[0] - a[0], b[1] - a[1]], [c[0] - b[0], c[1] - b[1]]);
let turn = (u[0] * v[1] - u[1] * v[0]).atan2(u[0] * v[0] + u[1] * v[1]);
turn.abs() > CORNER_TURN
})
.collect()
}
fn copper_outline(regions: &[outlines::Region], z: f64) -> Vec<Vec<Vec3>> {
outlines::union_boundary(regions)
.into_iter()
.map(|line| line.into_iter().map(|p| Vec3::new(p[0], p[1], z)).collect())
.collect()
}
fn copper_face(layer: u8, layer_count: u8) -> f64 {
let (lo, hi) = copper_span(layer, layer_count);
if layer > 0 && layer + 1 >= layer_count { lo } else { hi }
}
fn copper_layer(
board: &Board,
copper: &[CopperItem],
nets: &[Option<String>],
layer: u8,
) -> Option<Body> {
let (lo, hi) = copper_span(layer, board.layer_count);
let mut by_net: BTreeMap<Option<&str>, (Vec<usize>, Vec<&board::FillPiece>)> = BTreeMap::new();
for (i, item) in copper.iter().enumerate() {
if item.layers.0 <= layer && layer <= item.layers.1 {
by_net.entry(nets[i].as_deref()).or_default().0.push(i);
}
}
for zone in board.zones.iter().filter(|z| z.layer == layer) {
by_net.entry(Some(zone.net.as_str())).or_default().1.extend(&zone.fill);
}
let name = format!("{BOARD_SOLID_PREFIX}{}", board::layer_name(layer, board.layer_count));
let mut soup = Soup::default();
let mut faces: Vec<String> = Vec::with_capacity(by_net.len());
let mut regions: Vec<outlines::Region> = Vec::new();
for (net, (items, fill)) in &by_net {
let face = faces.len() as u32;
let mut drawn = false;
for &i in items {
let outline = shape_loop(&copper[i].shape);
if outline.len() >= 3 {
soup.prism(&outline, lo, hi, face);
regions.push(outlines::Region::Convex(outline));
drawn = true;
}
}
for piece in fill {
if piece.outer.len() >= 3 {
soup.slab(piece, lo, hi, face);
let rings = std::iter::once(&piece.outer)
.chain(&piece.holes)
.map(|ring| ring.iter().copied().map(xy).collect())
.collect();
regions.push(outlines::Region::Fill(rings));
drawn = true;
}
}
if drawn {
faces.push(format!("{name}{FACE_SEPARATOR}{}", net.unwrap_or(NO_NET_FACE)));
}
}
let mut body = body(name, COPPER_COLOR, faces, soup)?;
body.aux_from = body.payload.edges.len();
for line in copper_outline(®ions, copper_face(layer, board.layer_count)) {
let id = body.payload.edges.len() as u64;
body.payload.edges.push((id, None, line));
}
Some(body)
}
fn mask_openings(board: &Board, top: bool) -> impl Iterator<Item = Shape> + '_ {
let side = if top { 0 } else { board.layer_count.saturating_sub(1) };
board.placements.iter().flat_map(move |placement| {
placement.footprint.pads.iter().filter_map(move |pad| {
let (from, to) = placement.pad_layers(pad, board.layer_count);
let reaches = (from..=to).contains(&side) && (top || side != 0);
(reaches || (!top && pad.drill.is_some()))
.then(|| grown(placement.pad_shape(pad), board.rules.mask_expansion))
})
})
}
fn mask_layer(board: &Board, top: bool) -> Option<Body> {
let outline = oriented_outline(board)?;
let apertures: Vec<Vec<[f64; 2]>> = mask_openings(board, top)
.map(|shape| shape_loop(&shape))
.filter(|loop_xy| loop_xy.len() >= 3)
.collect();
if apertures.is_empty() {
return None;
}
let mut soup = Soup::default();
let (lo, hi) = side_span(top, 0, LACQUER_TOP);
soup.prism(&outline, lo, hi, 0);
let (open_lo, open_hi) = side_span(top, 0, LACQUER_TOP + board::COPPER_THICKNESS);
for loop_xy in &apertures {
soup.prism(loop_xy, open_lo, open_hi, 1);
}
let name = mask_solid(top);
let faces = vec![
format!("{name}{FACE_SEPARATOR}{LACQUER_FACE}"),
format!("{name}{FACE_SEPARATOR}{OPENING_FACE}"),
];
body(name, MASK_COLOR, faces, soup)
}
fn silkscreen(board: &Board, top: bool) -> Option<Body> {
let (lo, hi) = side_span(top, LACQUER_TOP, LACQUER_TOP + SILK_THICKNESS);
let mut soup = Soup::default();
for placement in board.placements.iter().filter(|p| p.bottom != top) {
for line in &placement.footprint.silk {
for pair in line.windows(2) {
let shape = Shape::segment(
placement.transform(pair[0]),
placement.transform(pair[1]),
brep_ecad_core::fabrication::SILK_WIDTH,
);
let loop_xy = shape_loop(&shape);
if loop_xy.len() >= 3 {
soup.prism(&loop_xy, lo, hi, 0);
}
}
}
}
body(silk_solid(top), SILK_COLOR, vec!["Ink".to_string()], soup)
}
fn via_barrels(board: &Board) -> Option<Body> {
let (lo, hi) = (mm(-board::SUBSTRATE_THICKNESS), 0.);
let mut soup = Soup::default();
let facets = ARC_FACETS * 2;
for via in &board.vias {
let centre = xy(via.at);
let inner = mm(via.drill.max(1) / 2).max(1e-4);
let outer = inner + mm(board::COPPER_THICKNESS);
let ring = |r: f64| -> Vec<[f64; 2]> {
(0..facets)
.map(|i| {
let t = std::f64::consts::TAU * i as f64 / facets as f64;
[centre[0] + r * t.cos(), centre[1] + r * t.sin()]
})
.collect()
};
let (out_ring, in_ring) = (ring(outer), ring(inner));
soup.walls(&out_ring, lo, hi, 0);
let mut bore = in_ring.clone();
bore.reverse();
soup.walls(&bore, lo, hi, 0);
for (z, up) in [(hi, true), (lo, false)] {
for i in 0..facets {
let j = (i + 1) % facets;
let (a, b) = (in_ring[i], in_ring[j]);
let (c, d) = (out_ring[j], out_ring[i]);
let quad = [
[a[0], a[1], z],
[b[0], b[1], z],
[c[0], c[1], z],
[d[0], d[1], z],
];
if up {
soup.quad(quad[0], quad[1], quad[2], quad[3], 0);
} else {
soup.quad(quad[3], quad[2], quad[1], quad[0], 0);
}
}
}
}
body(VIAS_SOLID.to_string(), COPPER_COLOR, vec!["Barrel".to_string()], soup)
}
fn copper_and_nets(
board: &Board,
netlist: Option<&Netlist>,
) -> (Vec<CopperItem>, Vec<Option<String>>) {
let Some(netlist) = netlist else {
let copper = board.copper();
let nets = vec![None; copper.len()];
return (copper, nets);
};
let connectivity = board.connectivity(netlist);
let nets = connectivity
.islands
.iter()
.map(|&island| connectivity.island_net(island).map(str::to_owned))
.collect();
(connectivity.items, nets)
}
fn bodies(board: &Board, netlist: Option<&Netlist>) -> Vec<Body> {
let (copper, nets) = copper_and_nets(board, netlist);
let mut out = Vec::new();
out.extend(substrate(board));
for layer in 0..board.layer_count.max(1) {
out.extend(copper_layer(board, &copper, &nets, layer));
}
out.extend(via_barrels(board));
for top in [true, false] {
out.extend(mask_layer(board, top));
}
for top in [true, false] {
out.extend(silkscreen(board, top));
}
out
}
fn default_visible(name: &str) -> bool {
!name.ends_with(".Mask")
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct BoardBuild {
pub bodies: usize,
pub triangles: usize,
pub edges: usize,
pub edge_segments: usize,
}
impl EngineState {
pub fn refresh_board_geometry(&mut self) -> BoardBuild {
let block = self.history.pcb_block();
if block.is_none() && self.board_displays.is_empty() {
return BoardBuild::default();
}
let document = block
.cloned()
.and_then(|value| brep_ecad_core::Document::from_value(value).ok());
let (board, netlist) = match document {
Some(document) => {
let netlist = (!document.board.placements.is_empty()).then(|| document.netlist());
(Some(document.board), netlist)
}
None => (None, None),
};
let pin_nets = netlist
.as_ref()
.map(brep_ecad_core::board::pin_nets)
.unwrap_or_default();
let rebuilt = board != self.board_built_from || pin_nets != self.board_pin_nets;
if rebuilt {
self.board_pin_nets = pin_nets;
self.rebuild_board_displays(board, netlist.as_ref());
}
let mut seated = false;
for index in 0..self.board_displays.len() {
let name = self.board_displays[index].name.clone();
let resident = self
.scene
.solid(&name)
.map(|solid| (solid.visible, solid.color_override));
if let Some((visible, _)) = resident {
self.board_displays[index].visible = visible;
}
if !rebuilt && resident.is_some() {
continue;
}
let mut fresh = self.board_displays[index].clone();
if let Some((_, color)) = resident {
fresh.color_override = color;
}
self.scene.insert_solid(fresh);
seated = true;
}
if seated {
self.sync_colors_from_metadata();
}
self.dirty |= seated;
self.board_build
}
fn rebuild_board_displays(&mut self, board: Option<Board>, netlist: Option<&Netlist>) {
let built: Vec<Body> = board
.as_ref()
.map(|board| bodies(board, netlist))
.unwrap_or_default();
let mut build = BoardBuild::default();
let was_visible: std::collections::HashMap<&str, bool> = self
.board_displays
.iter()
.map(|display| (display.name.as_str(), display.visible))
.collect();
let displays: Vec<crate::scene::SolidDisplay> = built
.into_iter()
.map(|Body { name, color, payload, aux_from }| {
build.bodies += 1;
build.triangles += payload.mesh.face_ids.len();
let mut solid = crate::scene::solid_display_from_payload(&name, payload);
for edge in solid.edges.iter_mut().skip(aux_from) {
edge.aux = true;
}
build.edges += solid.edges.len();
build.edge_segments += solid.edges.iter().map(|e| e.polyline.len().saturating_sub(1)).sum::<usize>();
solid.color_override = Some(color);
for face in &mut solid.faces {
face.color_override = default_color(&face.name);
}
solid.visible = was_visible
.get(name.as_str())
.copied()
.unwrap_or_else(|| default_visible(&name));
solid
})
.collect();
let fed: Vec<String> = displays.iter().map(|solid| solid.name.clone()).collect();
for name in std::mem::replace(&mut self.shown_board_ids, fed) {
if !self.shown_board_ids.contains(&name) {
self.scene.remove_solid(&name);
}
}
self.board_displays = displays;
self.board_built_from = board;
self.board_build = build;
self.sync_view_frame();
self.dirty = true;
}
pub fn board_build(&self) -> BoardBuild {
self.board_build
}
pub(super) fn remember_board_visibility(&mut self, name: &str, visible: bool) {
if !name.starts_with(BOARD_SOLID_PREFIX) {
return;
}
if let Some(display) = self
.board_displays
.iter_mut()
.find(|display| display.name == name)
{
display.visible = visible;
}
}
pub(crate) fn board_step_solids(&self) -> Option<step_solids::BoardStepSolids> {
let document = brep_ecad_core::Document::from_value(self.history.pcb_block()?.clone()).ok()?;
let netlist = (!document.board.placements.is_empty()).then(|| document.netlist());
step_solids::board_step_solids(&document.board, netlist.as_ref())
}
pub fn board_solid_names(&self) -> &[String] {
&self.shown_board_ids
}
}
mod outlines;
mod step_solids;