use std::collections::HashSet;
use crate::io::PcbDoc;
use crate::records::pcb::PcbRecord;
use crate::types::{Coord, CoordPoint, CoordRect, Layer};
use super::types::Grid;
#[derive(Debug, Clone)]
pub enum PlacementAnchor {
Absolute(CoordPoint),
NearComponent {
designator: String,
offset: CoordPoint,
},
AlignX { designator: String, offset: Coord },
AlignY { designator: String, offset: Coord },
BoardEdge { edge: BoardEdge, offset: Coord },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoardEdge {
Left,
Right,
Top,
Bottom,
}
impl BoardEdge {
pub fn try_parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"left" | "left-edge" | "l" => Some(BoardEdge::Left),
"right" | "right-edge" | "r" => Some(BoardEdge::Right),
"top" | "top-edge" | "t" => Some(BoardEdge::Top),
"bottom" | "bottom-edge" | "b" => Some(BoardEdge::Bottom),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct ConnectedRoutes {
pub tracks: Vec<usize>,
pub vias: Vec<usize>,
pub nets: HashSet<String>,
}
impl ConnectedRoutes {
pub fn has_connections(&self) -> bool {
!self.tracks.is_empty() || !self.vias.is_empty()
}
pub fn count(&self) -> usize {
self.tracks.len() + self.vias.len()
}
}
#[derive(Debug, Clone)]
pub struct ComponentPosition {
pub x: Coord,
pub y: Coord,
pub rotation: f64,
pub layer: Layer,
}
impl Default for ComponentPosition {
fn default() -> Self {
Self {
x: Coord::ZERO,
y: Coord::ZERO,
rotation: 0.0,
layer: Layer::TOP_LAYER,
}
}
}
pub struct PcbPlacementEngine {
grid: Grid,
board_bounds: Option<CoordRect>,
}
impl Default for PcbPlacementEngine {
fn default() -> Self {
Self::new()
}
}
impl PcbPlacementEngine {
pub fn new() -> Self {
Self {
grid: Grid::default(),
board_bounds: None,
}
}
pub fn set_grid(&mut self, grid: Grid) {
self.grid = grid;
}
pub fn set_grid_mm(&mut self, spacing_mm: f64) {
self.grid = Grid {
spacing: Coord::from_mms(spacing_mm),
snap_enabled: true,
};
}
pub fn grid(&self) -> &Grid {
&self.grid
}
pub fn set_board_bounds(&mut self, bounds: CoordRect) {
self.board_bounds = Some(bounds);
}
pub fn calculate_board_bounds(&mut self, pcb: &PcbDoc) {
let mut bounds = CoordRect::EMPTY;
for component in &pcb.components {
if let Some(pos) = Self::get_component_position_static(component) {
let _point = CoordPoint::new(pos.x, pos.y);
if bounds.is_empty() {
bounds = CoordRect::from_xywh(pos.x, pos.y, Coord::ZERO, Coord::ZERO);
} else {
bounds =
bounds.union(CoordRect::from_xywh(pos.x, pos.y, Coord::ZERO, Coord::ZERO));
}
}
}
if !bounds.is_empty() {
let margin = Coord::from_mils(500.0);
bounds = CoordRect::from_points(
bounds.location1.x - margin,
bounds.location1.y - margin,
bounds.location2.x + margin,
bounds.location2.y + margin,
);
}
self.board_bounds = Some(bounds);
}
pub fn snap_to_grid(&self, point: CoordPoint) -> CoordPoint {
self.grid.snap(point)
}
pub fn get_component_position(
&self,
pcb: &PcbDoc,
designator: &str,
) -> Option<ComponentPosition> {
pcb.components
.iter()
.find(|c| c.designator.eq_ignore_ascii_case(designator))
.and_then(Self::get_component_position_static)
}
fn get_component_position_static(
component: &crate::io::PcbDocComponent,
) -> Option<ComponentPosition> {
let x = component.params.get("X")?.as_coord_or(Coord::ZERO);
let y = component.params.get("Y")?.as_coord_or(Coord::ZERO);
let rotation = component
.params
.get("ROTATION")
.and_then(|v| v.as_str().parse::<f64>().ok())
.unwrap_or(0.0);
let layer = component
.params
.get("LAYER")
.and_then(|v| Layer::from_name(v.as_str()))
.unwrap_or(Layer::TOP_LAYER);
Some(ComponentPosition {
x,
y,
rotation,
layer,
})
}
pub fn resolve_anchor(
&self,
pcb: &PcbDoc,
anchor: &PlacementAnchor,
current_pos: Option<&ComponentPosition>,
) -> Result<CoordPoint, String> {
match anchor {
PlacementAnchor::Absolute(point) => Ok(self.grid.snap(*point)),
PlacementAnchor::NearComponent { designator, offset } => {
let ref_pos = self
.get_component_position(pcb, designator)
.ok_or_else(|| format!("Component '{}' not found", designator))?;
let point = CoordPoint::new(ref_pos.x + offset.x, ref_pos.y + offset.y);
Ok(self.grid.snap(point))
}
PlacementAnchor::AlignX { designator, offset } => {
let ref_pos = self
.get_component_position(pcb, designator)
.ok_or_else(|| format!("Component '{}' not found", designator))?;
let current_y = current_pos.map(|p| p.y).unwrap_or(Coord::ZERO);
let point = CoordPoint::new(ref_pos.x + *offset, current_y);
Ok(self.grid.snap(point))
}
PlacementAnchor::AlignY { designator, offset } => {
let ref_pos = self
.get_component_position(pcb, designator)
.ok_or_else(|| format!("Component '{}' not found", designator))?;
let current_x = current_pos.map(|p| p.x).unwrap_or(Coord::ZERO);
let point = CoordPoint::new(current_x, ref_pos.y + *offset);
Ok(self.grid.snap(point))
}
PlacementAnchor::BoardEdge { edge, offset } => {
let bounds = self
.board_bounds
.ok_or_else(|| "Board bounds not set".to_string())?;
let current_x = current_pos.map(|p| p.x).unwrap_or(bounds.center().x);
let current_y = current_pos.map(|p| p.y).unwrap_or(bounds.center().y);
let point = match edge {
BoardEdge::Left => CoordPoint::new(bounds.location1.x + *offset, current_y),
BoardEdge::Right => CoordPoint::new(bounds.location2.x - *offset, current_y),
BoardEdge::Top => CoordPoint::new(current_x, bounds.location2.y - *offset),
BoardEdge::Bottom => CoordPoint::new(current_x, bounds.location1.y + *offset),
};
Ok(self.grid.snap(point))
}
}
}
pub fn find_connected_routes(&self, pcb: &PcbDoc, designator: &str) -> ConnectedRoutes {
let mut result = ConnectedRoutes {
tracks: Vec::new(),
vias: Vec::new(),
nets: HashSet::new(),
};
let component = match pcb
.components
.iter()
.find(|c| c.designator.eq_ignore_ascii_case(designator))
{
Some(c) => c,
None => return result,
};
let comp_pos = match Self::get_component_position_static(component) {
Some(p) => p,
None => return result,
};
let pad_locations = self.get_component_pad_locations(pcb, component, &comp_pos);
let tolerance = Coord::from_mils(1.0);
for (i, primitive) in pcb.primitives.iter().enumerate() {
if let PcbRecord::Track(track) = primitive {
for pad_loc in &pad_locations {
if self.point_near_point(track.start, *pad_loc, tolerance)
|| self.point_near_point(track.end, *pad_loc, tolerance)
{
result.tracks.push(i);
break;
}
}
}
}
for (i, primitive) in pcb.primitives.iter().enumerate() {
if let PcbRecord::Via(via) = primitive {
for pad_loc in &pad_locations {
if self.point_near_point(via.location, *pad_loc, tolerance) {
result.vias.push(i);
break;
}
}
}
}
result
}
fn get_component_pad_locations(
&self,
_pcb: &PcbDoc,
_component: &crate::io::PcbDocComponent,
comp_pos: &ComponentPosition,
) -> Vec<CoordPoint> {
vec![CoordPoint::new(comp_pos.x, comp_pos.y)]
}
fn point_near_point(&self, p1: CoordPoint, p2: CoordPoint, tolerance: Coord) -> bool {
let dx = (p1.x - p2.x).abs();
let dy = (p1.y - p2.y).abs();
dx.to_raw() <= tolerance.to_raw() && dy.to_raw() <= tolerance.to_raw()
}
pub fn list_components(&self, pcb: &PcbDoc) -> Vec<(String, ComponentPosition)> {
pcb.components
.iter()
.filter_map(|c| {
Self::get_component_position_static(c).map(|pos| (c.designator.clone(), pos))
})
.collect()
}
pub fn find_component<'a>(
&self,
pcb: &'a PcbDoc,
designator: &str,
) -> Option<&'a crate::io::PcbDocComponent> {
pcb.components
.iter()
.find(|c| c.designator.eq_ignore_ascii_case(designator))
}
}
pub fn parse_position(s: &str) -> Result<CoordPoint, String> {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() != 2 {
return Err(format!(
"Invalid position format: '{}'. Expected 'X,Y' (e.g., '20mm,30mm')",
s
));
}
let x = parse_coord(parts[0].trim())?;
let y = parse_coord(parts[1].trim())?;
Ok(CoordPoint::new(x, y))
}
pub fn parse_coord(s: &str) -> Result<Coord, String> {
let s = s.trim().to_lowercase();
if s.ends_with("mm") {
let val: f64 = s
.trim_end_matches("mm")
.trim()
.parse()
.map_err(|_| format!("Invalid coordinate: {}", s))?;
Ok(Coord::from_mms(val))
} else if s.ends_with("mil") {
let val: f64 = s
.trim_end_matches("mil")
.trim()
.parse()
.map_err(|_| format!("Invalid coordinate: {}", s))?;
Ok(Coord::from_mils(val))
} else if s.ends_with("in") {
let val: f64 = s
.trim_end_matches("in")
.trim()
.parse()
.map_err(|_| format!("Invalid coordinate: {}", s))?;
Ok(Coord::from_inches(val))
} else {
let val: f64 = s
.parse()
.map_err(|_| format!("Invalid coordinate: {} (use '10mm', '100mil', etc.)", s))?;
Ok(Coord::from_mils(val))
}
}
pub fn parse_offset(s: &str) -> Result<CoordPoint, String> {
if s.contains(',') {
parse_position(s)
} else {
let coord = parse_coord(s)?;
Ok(CoordPoint::new(coord, Coord::ZERO))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_coord() {
assert!((parse_coord("10mm").unwrap().to_mms() - 10.0).abs() < 0.001);
assert!((parse_coord("100mil").unwrap().to_mils() - 100.0).abs() < 0.001);
assert!((parse_coord("1in").unwrap().to_mils() - 1000.0).abs() < 0.001);
assert!((parse_coord("50").unwrap().to_mils() - 50.0).abs() < 0.001);
}
#[test]
fn test_parse_position() {
let pos = parse_position("10mm,20mm").unwrap();
assert!((pos.x.to_mms() - 10.0).abs() < 0.001);
assert!((pos.y.to_mms() - 20.0).abs() < 0.001);
}
#[test]
fn test_board_edge_parse() {
assert_eq!(BoardEdge::try_parse("left"), Some(BoardEdge::Left));
assert_eq!(BoardEdge::try_parse("left-edge"), Some(BoardEdge::Left));
assert_eq!(BoardEdge::try_parse("TOP"), Some(BoardEdge::Top));
assert_eq!(BoardEdge::try_parse("invalid"), None);
}
}