1use std::collections::HashSet;
10
11use crate::io::PcbDoc;
12use crate::records::pcb::PcbRecord;
13use crate::types::{Coord, CoordPoint, CoordRect, Layer};
14
15use super::types::Grid;
16
17#[derive(Debug, Clone)]
19pub enum PlacementAnchor {
20 Absolute(CoordPoint),
22 NearComponent {
24 designator: String,
25 offset: CoordPoint,
26 },
27 AlignX { designator: String, offset: Coord },
29 AlignY { designator: String, offset: Coord },
31 BoardEdge { edge: BoardEdge, offset: Coord },
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum BoardEdge {
38 Left,
39 Right,
40 Top,
41 Bottom,
42}
43
44impl BoardEdge {
45 pub fn try_parse(s: &str) -> Option<Self> {
47 match s.to_lowercase().as_str() {
48 "left" | "left-edge" | "l" => Some(BoardEdge::Left),
49 "right" | "right-edge" | "r" => Some(BoardEdge::Right),
50 "top" | "top-edge" | "t" => Some(BoardEdge::Top),
51 "bottom" | "bottom-edge" | "b" => Some(BoardEdge::Bottom),
52 _ => None,
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct ConnectedRoutes {
60 pub tracks: Vec<usize>,
62 pub vias: Vec<usize>,
64 pub nets: HashSet<String>,
66}
67
68impl ConnectedRoutes {
69 pub fn has_connections(&self) -> bool {
71 !self.tracks.is_empty() || !self.vias.is_empty()
72 }
73
74 pub fn count(&self) -> usize {
76 self.tracks.len() + self.vias.len()
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct ComponentPosition {
83 pub x: Coord,
84 pub y: Coord,
85 pub rotation: f64,
86 pub layer: Layer,
87}
88
89impl Default for ComponentPosition {
90 fn default() -> Self {
91 Self {
92 x: Coord::ZERO,
93 y: Coord::ZERO,
94 rotation: 0.0,
95 layer: Layer::TOP_LAYER,
96 }
97 }
98}
99
100pub struct PcbPlacementEngine {
102 grid: Grid,
104 board_bounds: Option<CoordRect>,
106}
107
108impl Default for PcbPlacementEngine {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114impl PcbPlacementEngine {
115 pub fn new() -> Self {
117 Self {
118 grid: Grid::default(),
119 board_bounds: None,
120 }
121 }
122
123 pub fn set_grid(&mut self, grid: Grid) {
125 self.grid = grid;
126 }
127
128 pub fn set_grid_mm(&mut self, spacing_mm: f64) {
130 self.grid = Grid {
131 spacing: Coord::from_mms(spacing_mm),
132 snap_enabled: true,
133 };
134 }
135
136 pub fn grid(&self) -> &Grid {
138 &self.grid
139 }
140
141 pub fn set_board_bounds(&mut self, bounds: CoordRect) {
143 self.board_bounds = Some(bounds);
144 }
145
146 pub fn calculate_board_bounds(&mut self, pcb: &PcbDoc) {
148 let mut bounds = CoordRect::EMPTY;
150
151 for component in &pcb.components {
152 if let Some(pos) = Self::get_component_position_static(component) {
153 let _point = CoordPoint::new(pos.x, pos.y);
154 if bounds.is_empty() {
155 bounds = CoordRect::from_xywh(pos.x, pos.y, Coord::ZERO, Coord::ZERO);
156 } else {
157 bounds =
158 bounds.union(CoordRect::from_xywh(pos.x, pos.y, Coord::ZERO, Coord::ZERO));
159 }
160 }
161 }
162
163 if !bounds.is_empty() {
165 let margin = Coord::from_mils(500.0);
166 bounds = CoordRect::from_points(
167 bounds.location1.x - margin,
168 bounds.location1.y - margin,
169 bounds.location2.x + margin,
170 bounds.location2.y + margin,
171 );
172 }
173
174 self.board_bounds = Some(bounds);
175 }
176
177 pub fn snap_to_grid(&self, point: CoordPoint) -> CoordPoint {
179 self.grid.snap(point)
180 }
181
182 pub fn get_component_position(
184 &self,
185 pcb: &PcbDoc,
186 designator: &str,
187 ) -> Option<ComponentPosition> {
188 pcb.components
189 .iter()
190 .find(|c| c.designator.eq_ignore_ascii_case(designator))
191 .and_then(Self::get_component_position_static)
192 }
193
194 fn get_component_position_static(
196 component: &crate::io::PcbDocComponent,
197 ) -> Option<ComponentPosition> {
198 let x = component.params.get("X")?.as_coord_or(Coord::ZERO);
199 let y = component.params.get("Y")?.as_coord_or(Coord::ZERO);
200 let rotation = component
201 .params
202 .get("ROTATION")
203 .and_then(|v| v.as_str().parse::<f64>().ok())
204 .unwrap_or(0.0);
205 let layer = component
206 .params
207 .get("LAYER")
208 .and_then(|v| Layer::from_name(v.as_str()))
209 .unwrap_or(Layer::TOP_LAYER);
210
211 Some(ComponentPosition {
212 x,
213 y,
214 rotation,
215 layer,
216 })
217 }
218
219 pub fn resolve_anchor(
221 &self,
222 pcb: &PcbDoc,
223 anchor: &PlacementAnchor,
224 current_pos: Option<&ComponentPosition>,
225 ) -> Result<CoordPoint, String> {
226 match anchor {
227 PlacementAnchor::Absolute(point) => Ok(self.grid.snap(*point)),
228 PlacementAnchor::NearComponent { designator, offset } => {
229 let ref_pos = self
230 .get_component_position(pcb, designator)
231 .ok_or_else(|| format!("Component '{}' not found", designator))?;
232 let point = CoordPoint::new(ref_pos.x + offset.x, ref_pos.y + offset.y);
233 Ok(self.grid.snap(point))
234 }
235 PlacementAnchor::AlignX { designator, offset } => {
236 let ref_pos = self
237 .get_component_position(pcb, designator)
238 .ok_or_else(|| format!("Component '{}' not found", designator))?;
239 let current_y = current_pos.map(|p| p.y).unwrap_or(Coord::ZERO);
240 let point = CoordPoint::new(ref_pos.x + *offset, current_y);
241 Ok(self.grid.snap(point))
242 }
243 PlacementAnchor::AlignY { designator, offset } => {
244 let ref_pos = self
245 .get_component_position(pcb, designator)
246 .ok_or_else(|| format!("Component '{}' not found", designator))?;
247 let current_x = current_pos.map(|p| p.x).unwrap_or(Coord::ZERO);
248 let point = CoordPoint::new(current_x, ref_pos.y + *offset);
249 Ok(self.grid.snap(point))
250 }
251 PlacementAnchor::BoardEdge { edge, offset } => {
252 let bounds = self
253 .board_bounds
254 .ok_or_else(|| "Board bounds not set".to_string())?;
255 let current_x = current_pos.map(|p| p.x).unwrap_or(bounds.center().x);
256 let current_y = current_pos.map(|p| p.y).unwrap_or(bounds.center().y);
257
258 let point = match edge {
259 BoardEdge::Left => CoordPoint::new(bounds.location1.x + *offset, current_y),
260 BoardEdge::Right => CoordPoint::new(bounds.location2.x - *offset, current_y),
261 BoardEdge::Top => CoordPoint::new(current_x, bounds.location2.y - *offset),
262 BoardEdge::Bottom => CoordPoint::new(current_x, bounds.location1.y + *offset),
263 };
264 Ok(self.grid.snap(point))
265 }
266 }
267 }
268
269 pub fn find_connected_routes(&self, pcb: &PcbDoc, designator: &str) -> ConnectedRoutes {
271 let mut result = ConnectedRoutes {
272 tracks: Vec::new(),
273 vias: Vec::new(),
274 nets: HashSet::new(),
275 };
276
277 let component = match pcb
279 .components
280 .iter()
281 .find(|c| c.designator.eq_ignore_ascii_case(designator))
282 {
283 Some(c) => c,
284 None => return result,
285 };
286
287 let comp_pos = match Self::get_component_position_static(component) {
289 Some(p) => p,
290 None => return result,
291 };
292
293 let pad_locations = self.get_component_pad_locations(pcb, component, &comp_pos);
297
298 let tolerance = Coord::from_mils(1.0);
300
301 for (i, primitive) in pcb.primitives.iter().enumerate() {
303 if let PcbRecord::Track(track) = primitive {
304 for pad_loc in &pad_locations {
305 if self.point_near_point(track.start, *pad_loc, tolerance)
306 || self.point_near_point(track.end, *pad_loc, tolerance)
307 {
308 result.tracks.push(i);
309 break;
310 }
311 }
312 }
313 }
314
315 for (i, primitive) in pcb.primitives.iter().enumerate() {
317 if let PcbRecord::Via(via) = primitive {
318 for pad_loc in &pad_locations {
319 if self.point_near_point(via.location, *pad_loc, tolerance) {
320 result.vias.push(i);
321 break;
322 }
323 }
324 }
325 }
326
327 result
328 }
329
330 fn get_component_pad_locations(
332 &self,
333 _pcb: &PcbDoc,
334 _component: &crate::io::PcbDocComponent,
335 comp_pos: &ComponentPosition,
336 ) -> Vec<CoordPoint> {
337 vec![CoordPoint::new(comp_pos.x, comp_pos.y)]
340 }
341
342 fn point_near_point(&self, p1: CoordPoint, p2: CoordPoint, tolerance: Coord) -> bool {
344 let dx = (p1.x - p2.x).abs();
345 let dy = (p1.y - p2.y).abs();
346 dx.to_raw() <= tolerance.to_raw() && dy.to_raw() <= tolerance.to_raw()
347 }
348
349 pub fn list_components(&self, pcb: &PcbDoc) -> Vec<(String, ComponentPosition)> {
351 pcb.components
352 .iter()
353 .filter_map(|c| {
354 Self::get_component_position_static(c).map(|pos| (c.designator.clone(), pos))
355 })
356 .collect()
357 }
358
359 pub fn find_component<'a>(
361 &self,
362 pcb: &'a PcbDoc,
363 designator: &str,
364 ) -> Option<&'a crate::io::PcbDocComponent> {
365 pcb.components
366 .iter()
367 .find(|c| c.designator.eq_ignore_ascii_case(designator))
368 }
369}
370
371pub fn parse_position(s: &str) -> Result<CoordPoint, String> {
373 let parts: Vec<&str> = s.split(',').collect();
374 if parts.len() != 2 {
375 return Err(format!(
376 "Invalid position format: '{}'. Expected 'X,Y' (e.g., '20mm,30mm')",
377 s
378 ));
379 }
380
381 let x = parse_coord(parts[0].trim())?;
382 let y = parse_coord(parts[1].trim())?;
383
384 Ok(CoordPoint::new(x, y))
385}
386
387pub fn parse_coord(s: &str) -> Result<Coord, String> {
389 let s = s.trim().to_lowercase();
390
391 if s.ends_with("mm") {
392 let val: f64 = s
393 .trim_end_matches("mm")
394 .trim()
395 .parse()
396 .map_err(|_| format!("Invalid coordinate: {}", s))?;
397 Ok(Coord::from_mms(val))
398 } else if s.ends_with("mil") {
399 let val: f64 = s
400 .trim_end_matches("mil")
401 .trim()
402 .parse()
403 .map_err(|_| format!("Invalid coordinate: {}", s))?;
404 Ok(Coord::from_mils(val))
405 } else if s.ends_with("in") {
406 let val: f64 = s
407 .trim_end_matches("in")
408 .trim()
409 .parse()
410 .map_err(|_| format!("Invalid coordinate: {}", s))?;
411 Ok(Coord::from_inches(val))
412 } else {
413 let val: f64 = s
415 .parse()
416 .map_err(|_| format!("Invalid coordinate: {} (use '10mm', '100mil', etc.)", s))?;
417 Ok(Coord::from_mils(val))
418 }
419}
420
421pub fn parse_offset(s: &str) -> Result<CoordPoint, String> {
423 if s.contains(',') {
424 parse_position(s)
425 } else {
426 let coord = parse_coord(s)?;
428 Ok(CoordPoint::new(coord, Coord::ZERO))
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn test_parse_coord() {
438 assert!((parse_coord("10mm").unwrap().to_mms() - 10.0).abs() < 0.001);
439 assert!((parse_coord("100mil").unwrap().to_mils() - 100.0).abs() < 0.001);
440 assert!((parse_coord("1in").unwrap().to_mils() - 1000.0).abs() < 0.001);
441 assert!((parse_coord("50").unwrap().to_mils() - 50.0).abs() < 0.001);
442 }
443
444 #[test]
445 fn test_parse_position() {
446 let pos = parse_position("10mm,20mm").unwrap();
447 assert!((pos.x.to_mms() - 10.0).abs() < 0.001);
448 assert!((pos.y.to_mms() - 20.0).abs() < 0.001);
449 }
450
451 #[test]
452 fn test_board_edge_parse() {
453 assert_eq!(BoardEdge::try_parse("left"), Some(BoardEdge::Left));
454 assert_eq!(BoardEdge::try_parse("left-edge"), Some(BoardEdge::Left));
455 assert_eq!(BoardEdge::try_parse("TOP"), Some(BoardEdge::Top));
456 assert_eq!(BoardEdge::try_parse("invalid"), None);
457 }
458}