1use std::collections::HashMap;
4
5use crate::records::sch::{PinConglomerateFlags, SchComponent, SchPin, SchPrimitive, SchRecord};
6use crate::types::{Coord, CoordPoint, CoordRect};
7
8use super::types::{
9 DEFAULT_COMPONENT_SPACING_MILS, Direction, Grid, Orientation, PinLocation, PlacedComponent,
10 PlacementSuggestion,
11};
12
13pub struct LayoutEngine {
15 grid: Grid,
17 component_spacing: Coord,
19 component_bounds: HashMap<usize, CoordRect>,
21 sheet_bounds: CoordRect,
23}
24
25impl Default for LayoutEngine {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl LayoutEngine {
32 pub fn new() -> Self {
34 Self {
35 grid: Grid::default(),
36 component_spacing: Coord::from_mils(DEFAULT_COMPONENT_SPACING_MILS),
37 component_bounds: HashMap::new(),
38 sheet_bounds: CoordRect::from_xywh(
39 Coord::ZERO,
40 Coord::ZERO,
41 Coord::from_mils(11000.0), Coord::from_mils(8500.0),
43 ),
44 }
45 }
46
47 pub fn set_grid(&mut self, grid: Grid) {
49 self.grid = grid;
50 }
51
52 pub fn grid(&self) -> &Grid {
54 &self.grid
55 }
56
57 pub fn set_sheet_bounds(&mut self, bounds: CoordRect) {
59 self.sheet_bounds = bounds;
60 }
61
62 pub fn set_component_spacing(&mut self, spacing: Coord) {
64 self.component_spacing = spacing;
65 }
66
67 pub fn clear_cache(&mut self) {
69 self.component_bounds.clear();
70 }
71
72 fn get_record_bounds(&self, record: &SchRecord) -> CoordRect {
74 match record {
75 SchRecord::Pin(p) => p.calculate_bounds(),
76 SchRecord::Line(l) => l.calculate_bounds(),
77 SchRecord::Rectangle(r) => r.calculate_bounds(),
78 SchRecord::Polygon(p) => p.calculate_bounds(),
79 SchRecord::Polyline(p) => p.calculate_bounds(),
80 SchRecord::Arc(a) => a.calculate_bounds(),
81 SchRecord::Ellipse(e) => e.calculate_bounds(),
82 SchRecord::Label(l) => l.calculate_bounds(),
83 SchRecord::Wire(w) => w.calculate_bounds(),
84 SchRecord::Junction(j) => j.calculate_bounds(),
85 SchRecord::NetLabel(n) => n.calculate_bounds(),
86 SchRecord::PowerObject(p) => p.calculate_bounds(),
87 SchRecord::Port(p) => p.calculate_bounds(),
88 _ => CoordRect::empty(),
89 }
90 }
91
92 pub fn calculate_component_bounds(
94 &self,
95 component: &SchComponent,
96 primitives: &[SchRecord],
97 component_index: usize,
98 ) -> CoordRect {
99 let base_x = component.graphical.location_x;
100 let base_y = component.graphical.location_y;
101
102 let mut bounds = CoordRect::empty();
103
104 for (i, record) in primitives.iter().enumerate() {
106 if i == component_index {
107 continue; }
109
110 let owner_index = match record {
111 SchRecord::Pin(p) => p.graphical.base.owner_index,
112 SchRecord::Line(l) => l.graphical.base.owner_index,
113 SchRecord::Rectangle(r) => r.graphical.base.owner_index,
114 SchRecord::Polygon(p) => p.graphical.base.owner_index,
115 SchRecord::Polyline(p) => p.graphical.base.owner_index,
116 SchRecord::Arc(a) => a.graphical.base.owner_index,
117 SchRecord::Ellipse(e) => e.graphical.base.owner_index,
118 SchRecord::Label(l) => l.graphical.base.owner_index,
119 SchRecord::Designator(d) => d.param.label.graphical.base.owner_index,
120 SchRecord::Parameter(p) => p.label.graphical.base.owner_index,
121 _ => -1,
122 };
123
124 if owner_index == component_index as i32 {
125 let prim_bounds = self.get_record_bounds(record);
126 if !prim_bounds.is_empty() {
127 bounds = bounds.union(prim_bounds);
128 }
129 }
130 }
131
132 if bounds.is_empty() {
134 bounds = CoordRect::from_xywh(
135 Coord::from_raw(base_x),
136 Coord::from_raw(base_y),
137 Coord::from_mils(100.0),
138 Coord::from_mils(100.0),
139 );
140 }
141
142 bounds
143 }
144
145 pub fn get_placed_components(&self, primitives: &[SchRecord]) -> Vec<PlacedComponent> {
147 let mut placed = Vec::new();
148
149 for (i, record) in primitives.iter().enumerate() {
150 if let SchRecord::Component(component) = record {
151 let bounds = self.calculate_component_bounds(component, primitives, i);
152 let pin_locations = self.get_pin_locations(primitives, i);
153
154 let designator = self.find_designator(primitives, i);
156
157 placed.push(PlacedComponent {
158 index: i,
159 designator,
160 lib_reference: component.lib_reference.clone(),
161 bounds,
162 pin_locations,
163 });
164 }
165 }
166
167 placed
168 }
169
170 fn find_designator(&self, primitives: &[SchRecord], component_index: usize) -> String {
172 for (i, record) in primitives.iter().enumerate() {
173 if i == component_index {
174 continue;
175 }
176 if let SchRecord::Designator(d) = record {
177 if d.param.label.graphical.base.owner_index == component_index as i32 {
178 return d.param.value().to_string();
179 }
180 }
181 }
182 String::new()
183 }
184
185 fn get_pin_locations(
187 &self,
188 primitives: &[SchRecord],
189 component_index: usize,
190 ) -> Vec<PinLocation> {
191 let mut pins = Vec::new();
192
193 let component = match &primitives[component_index] {
195 SchRecord::Component(c) => c,
196 _ => return pins,
197 };
198 let _base_x = component.graphical.location_x;
199 let _base_y = component.graphical.location_y;
200
201 for (i, record) in primitives.iter().enumerate() {
202 if i == component_index {
203 continue;
204 }
205 if let SchRecord::Pin(pin) = record {
206 if pin.graphical.base.owner_index == component_index as i32 {
207 let pin_loc = self.calculate_pin_endpoint(pin);
208 let direction = self.get_pin_direction(pin);
209
210 pins.push(PinLocation {
211 designator: pin.designator.clone(),
212 name: pin.name.clone(),
213 location: pin_loc,
214 direction,
215 });
216 }
217 }
218 }
219
220 pins
221 }
222
223 fn calculate_pin_endpoint(&self, pin: &SchPin) -> CoordPoint {
225 let base_x = pin.graphical.location_x;
226 let base_y = pin.graphical.location_y;
227 let length = pin.pin_length;
228
229 let rotated = pin.pin_conglomerate.contains(PinConglomerateFlags::ROTATED);
231 let flipped = pin.pin_conglomerate.contains(PinConglomerateFlags::FLIPPED);
232
233 let (dx, dy) = match (rotated, flipped) {
234 (false, false) => (length, 0), (true, false) => (0, length), (false, true) => (-length, 0), (true, true) => (0, -length), };
239
240 CoordPoint::from_raw(base_x + dx, base_y + dy)
241 }
242
243 fn get_pin_direction(&self, pin: &SchPin) -> Direction {
245 let rotated = pin.pin_conglomerate.contains(PinConglomerateFlags::ROTATED);
246 let flipped = pin.pin_conglomerate.contains(PinConglomerateFlags::FLIPPED);
247
248 match (rotated, flipped) {
249 (false, false) => Direction::Right,
250 (true, false) => Direction::Up,
251 (false, true) => Direction::Left,
252 (true, true) => Direction::Down,
253 }
254 }
255
256 pub fn check_collision(
258 &self,
259 rect: CoordRect,
260 primitives: &[SchRecord],
261 exclude_index: Option<usize>,
262 ) -> bool {
263 let placed = self.get_placed_components(primitives);
264
265 for component in placed {
266 if Some(component.index) == exclude_index {
267 continue;
268 }
269
270 let expanded = CoordRect::from_points(
272 component.bounds.location1.x - self.component_spacing,
273 component.bounds.location1.y - self.component_spacing,
274 component.bounds.location2.x + self.component_spacing,
275 component.bounds.location2.y + self.component_spacing,
276 );
277
278 if expanded.intersects(rect) {
279 return true;
280 }
281 }
282
283 false
284 }
285
286 pub fn suggest_placement(
288 &self,
289 component_bounds: CoordRect,
290 primitives: &[SchRecord],
291 near_component: Option<&str>,
292 ) -> Vec<PlacementSuggestion> {
293 let mut suggestions = Vec::new();
294 let placed = self.get_placed_components(primitives);
295
296 let width = component_bounds.width();
298 let height = component_bounds.height();
299
300 if let Some(ref_designator) = near_component {
302 if let Some(ref_component) = placed.iter().find(|c| c.designator == ref_designator) {
303 suggestions.extend(self.suggest_near_component(
304 ref_component,
305 width,
306 height,
307 primitives,
308 ));
309 }
310 }
311
312 suggestions.extend(self.suggest_empty_regions(width, height, primitives));
314
315 suggestions.sort_by(|a, b| {
317 b.score
318 .partial_cmp(&a.score)
319 .unwrap_or(std::cmp::Ordering::Equal)
320 });
321
322 suggestions.truncate(5);
324 suggestions
325 }
326
327 fn suggest_near_component(
329 &self,
330 ref_component: &PlacedComponent,
331 width: Coord,
332 height: Coord,
333 primitives: &[SchRecord],
334 ) -> Vec<PlacementSuggestion> {
335 let mut suggestions = Vec::new();
336 let ref_bounds = ref_component.bounds;
337 let spacing = self.component_spacing;
338
339 let positions = [
341 (
342 CoordPoint::new(ref_bounds.location2.x + spacing, ref_bounds.location1.y),
343 "Right of",
344 0.9,
345 ),
346 (
347 CoordPoint::new(
348 ref_bounds.location1.x - width - spacing,
349 ref_bounds.location1.y,
350 ),
351 "Left of",
352 0.85,
353 ),
354 (
355 CoordPoint::new(ref_bounds.location1.x, ref_bounds.location2.y + spacing),
356 "Above",
357 0.8,
358 ),
359 (
360 CoordPoint::new(
361 ref_bounds.location1.x,
362 ref_bounds.location1.y - height - spacing,
363 ),
364 "Below",
365 0.75,
366 ),
367 ];
368
369 for (pos, direction, base_score) in positions {
370 let snapped = self.grid.snap(pos);
371 let test_bounds = CoordRect::from_xywh(snapped.x, snapped.y, width, height);
372
373 if !self.check_collision(test_bounds, primitives, None)
374 && self.sheet_bounds.contains(snapped)
375 {
376 suggestions.push(PlacementSuggestion {
377 location: snapped,
378 orientation: Orientation::Normal,
379 score: base_score,
380 reason: format!("{} {}", direction, ref_component.designator),
381 });
382 }
383 }
384
385 suggestions
386 }
387
388 fn suggest_empty_regions(
390 &self,
391 width: Coord,
392 height: Coord,
393 primitives: &[SchRecord],
394 ) -> Vec<PlacementSuggestion> {
395 let mut suggestions = Vec::new();
396 let placed = self.get_placed_components(primitives);
397
398 let mut occupied = CoordRect::empty();
400 for component in &placed {
401 occupied = occupied.union(component.bounds);
402 }
403
404 let grid_step = Coord::from_mils(500.0);
406 let margin = Coord::from_mils(200.0);
407
408 let mut y = self.sheet_bounds.location1.y + margin;
409 while y < self.sheet_bounds.location2.y - height - margin {
410 let mut x = self.sheet_bounds.location1.x + margin;
411 while x < self.sheet_bounds.location2.x - width - margin {
412 let pos = self.grid.snap(CoordPoint::new(x, y));
413 let test_bounds = CoordRect::from_xywh(pos.x, pos.y, width, height);
414
415 if !self.check_collision(test_bounds, primitives, None) {
416 let x_score = 1.0 - (pos.x.to_mils() / self.sheet_bounds.width().to_mils());
418 let y_score = pos.y.to_mils() / self.sheet_bounds.height().to_mils();
419 let score = (x_score + y_score) * 0.3;
420
421 suggestions.push(PlacementSuggestion {
422 location: pos,
423 orientation: Orientation::Normal,
424 score,
425 reason: format!(
426 "Empty region at ({:.0}, {:.0})",
427 pos.x.to_mils(),
428 pos.y.to_mils()
429 ),
430 });
431 }
432
433 x = x + grid_step;
434 }
435 y = y + grid_step;
436 }
437
438 suggestions.truncate(10);
440 suggestions
441 }
442
443 pub fn find_best_position(
445 &self,
446 component_bounds: CoordRect,
447 primitives: &[SchRecord],
448 ) -> Option<CoordPoint> {
449 let suggestions = self.suggest_placement(component_bounds, primitives, None);
450 suggestions.first().map(|s| s.location)
451 }
452
453 pub fn snap_to_grid(&self, point: CoordPoint) -> CoordPoint {
455 self.grid.snap(point)
456 }
457
458 pub fn is_on_grid(&self, point: CoordPoint) -> bool {
460 let snapped = self.grid.snap(point);
461 snapped.x == point.x && snapped.y == point.y
462 }
463
464 pub fn get_all_bounds(&self, primitives: &[SchRecord]) -> Vec<CoordRect> {
466 self.get_placed_components(primitives)
467 .into_iter()
468 .map(|c| c.bounds)
469 .collect()
470 }
471
472 pub fn local_to_absolute(
474 &self,
475 local: CoordPoint,
476 component_location: CoordPoint,
477 orientation: Orientation,
478 ) -> CoordPoint {
479 let rotated = local.rotate(CoordPoint::ZERO, orientation.rotation_degrees());
480
481 let mirrored = if orientation.is_mirrored() {
483 CoordPoint::new(-rotated.x, rotated.y)
484 } else {
485 rotated
486 };
487
488 mirrored.translate(component_location.x, component_location.y)
490 }
491
492 pub fn absolute_to_local(
494 &self,
495 absolute: CoordPoint,
496 component_location: CoordPoint,
497 orientation: Orientation,
498 ) -> CoordPoint {
499 let translated = absolute.translate(-component_location.x, -component_location.y);
501
502 let unmirrored = if orientation.is_mirrored() {
504 CoordPoint::new(-translated.x, translated.y)
505 } else {
506 translated
507 };
508
509 unmirrored.rotate(CoordPoint::ZERO, -orientation.rotation_degrees())
511 }
512}