Skip to main content

altium_format/edit/
layout.rs

1//! Layout engine for component placement and collision detection.
2
3use 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
13/// Layout engine for managing component placement on a schematic.
14pub struct LayoutEngine {
15    /// Grid configuration.
16    grid: Grid,
17    /// Minimum spacing between components.
18    component_spacing: Coord,
19    /// Cached component bounds.
20    component_bounds: HashMap<usize, CoordRect>,
21    /// Sheet bounds.
22    sheet_bounds: CoordRect,
23}
24
25impl Default for LayoutEngine {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl LayoutEngine {
32    /// Create a new layout engine with default settings.
33    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), // Default A4 landscape
42                Coord::from_mils(8500.0),
43            ),
44        }
45    }
46
47    /// Set the grid configuration.
48    pub fn set_grid(&mut self, grid: Grid) {
49        self.grid = grid;
50    }
51
52    /// Get the grid configuration.
53    pub fn grid(&self) -> &Grid {
54        &self.grid
55    }
56
57    /// Set the sheet bounds.
58    pub fn set_sheet_bounds(&mut self, bounds: CoordRect) {
59        self.sheet_bounds = bounds;
60    }
61
62    /// Set minimum component spacing.
63    pub fn set_component_spacing(&mut self, spacing: Coord) {
64        self.component_spacing = spacing;
65    }
66
67    /// Clear cached bounds.
68    pub fn clear_cache(&mut self) {
69        self.component_bounds.clear();
70    }
71
72    /// Get the bounds of a schematic record.
73    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    /// Calculate the bounding box for a component and its child primitives.
93    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        // Find all primitives owned by this component
105        for (i, record) in primitives.iter().enumerate() {
106            if i == component_index {
107                continue; // Skip the component itself
108            }
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 no child bounds found, use a default size
133        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    /// Get placed components from primitives.
146    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                // Find the designator
155                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    /// Find the designator for a component.
171    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    /// Get pin locations for a component.
186    fn get_pin_locations(
187        &self,
188        primitives: &[SchRecord],
189        component_index: usize,
190    ) -> Vec<PinLocation> {
191        let mut pins = Vec::new();
192
193        // Get component location for reference
194        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    /// Calculate the endpoint of a pin (where wires connect).
224    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        // Pin direction based on rotation in pin_conglomerate
230        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), // Right
235            (true, false) => (0, length),  // Up
236            (false, true) => (-length, 0), // Left
237            (true, true) => (0, -length),  // Down
238        };
239
240        CoordPoint::from_raw(base_x + dx, base_y + dy)
241    }
242
243    /// Get the direction a pin faces.
244    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    /// Check if a rectangle collides with any existing component.
257    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            // Expand bounds by spacing
271            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    /// Find placement suggestions for a component with given bounds.
287    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        // Component size
297        let width = component_bounds.width();
298        let height = component_bounds.height();
299
300        // If near_component is specified, prioritize positions near it
301        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        // Find empty regions on the sheet
313        suggestions.extend(self.suggest_empty_regions(width, height, primitives));
314
315        // Sort by score (higher is better)
316        suggestions.sort_by(|a, b| {
317            b.score
318                .partial_cmp(&a.score)
319                .unwrap_or(std::cmp::Ordering::Equal)
320        });
321
322        // Return top suggestions
323        suggestions.truncate(5);
324        suggestions
325    }
326
327    /// Suggest placements near a reference component.
328    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        // Try positions: right, left, above, below
340        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    /// Suggest placements in empty regions.
389    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        // Calculate occupied regions
399        let mut occupied = CoordRect::empty();
400        for component in &placed {
401            occupied = occupied.union(component.bounds);
402        }
403
404        // Try positions in a grid pattern across the sheet
405        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                    // Score based on position (prefer upper-left for new components)
417                    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        // Limit number of empty region suggestions
439        suggestions.truncate(10);
440        suggestions
441    }
442
443    /// Find the best position for a new component.
444    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    /// Align components to grid.
454    pub fn snap_to_grid(&self, point: CoordPoint) -> CoordPoint {
455        self.grid.snap(point)
456    }
457
458    /// Check if a point is on the grid.
459    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    /// Get all component bounds as rectangles for collision detection.
465    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    /// Transform a point from component-local to absolute coordinates.
473    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        // Apply mirroring if needed
482        let mirrored = if orientation.is_mirrored() {
483            CoordPoint::new(-rotated.x, rotated.y)
484        } else {
485            rotated
486        };
487
488        // Translate to component location
489        mirrored.translate(component_location.x, component_location.y)
490    }
491
492    /// Transform a point from absolute to component-local coordinates.
493    pub fn absolute_to_local(
494        &self,
495        absolute: CoordPoint,
496        component_location: CoordPoint,
497        orientation: Orientation,
498    ) -> CoordPoint {
499        // Translate to origin
500        let translated = absolute.translate(-component_location.x, -component_location.y);
501
502        // Reverse mirroring if needed
503        let unmirrored = if orientation.is_mirrored() {
504            CoordPoint::new(-translated.x, translated.y)
505        } else {
506            translated
507        };
508
509        // Reverse rotation
510        unmirrored.rotate(CoordPoint::ZERO, -orientation.rotation_degrees())
511    }
512}