Skip to main content

altium_format/edit/
routing.rs

1//! Routing engine for automatic wire routing between pins.
2
3use std::cmp::Ordering;
4use std::collections::{BinaryHeap, HashMap, HashSet};
5
6use crate::records::sch::SchRecord;
7use crate::types::{Coord, CoordPoint, CoordRect};
8
9use super::layout::LayoutEngine;
10use super::types::{Direction, Grid, RoutingPath, WireSegment};
11
12/// Cost multipliers for routing decisions.
13const STRAIGHT_COST: i64 = 1;
14const TURN_COST: i64 = 2;
15const CROSSING_COST: i64 = 5;
16
17/// A* node for pathfinding.
18#[derive(Clone, Eq, PartialEq)]
19struct PathNode {
20    position: CoordPoint,
21    g_cost: i64, // Actual cost from start
22    f_cost: i64, // Estimated total cost (g + heuristic)
23    direction: Option<Direction>,
24}
25
26impl Ord for PathNode {
27    fn cmp(&self, other: &Self) -> Ordering {
28        // Reverse ordering for min-heap
29        other.f_cost.cmp(&self.f_cost)
30    }
31}
32
33impl PartialOrd for PathNode {
34    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
35        Some(self.cmp(other))
36    }
37}
38
39/// Routing engine for wire connections.
40pub struct RoutingEngine {
41    /// Grid configuration for wire routing.
42    grid: Grid,
43    /// Obstacle bounds (components, existing wires).
44    obstacles: Vec<CoordRect>,
45    /// Existing wire segments.
46    existing_wires: Vec<WireSegment>,
47    /// Maximum routing iterations.
48    max_iterations: usize,
49}
50
51impl Default for RoutingEngine {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl RoutingEngine {
58    /// Create a new routing engine.
59    pub fn new() -> Self {
60        Self {
61            grid: Grid::default(),
62            obstacles: Vec::new(),
63            existing_wires: Vec::new(),
64            max_iterations: 10000,
65        }
66    }
67
68    /// Set the grid configuration.
69    pub fn set_grid(&mut self, grid: Grid) {
70        self.grid = grid;
71    }
72
73    /// Set maximum iterations for routing.
74    pub fn set_max_iterations(&mut self, max: usize) {
75        self.max_iterations = max;
76    }
77
78    /// Update obstacles from schematic primitives.
79    pub fn update_obstacles(&mut self, primitives: &[SchRecord], layout: &LayoutEngine) {
80        self.obstacles.clear();
81        self.existing_wires.clear();
82
83        // Add component bounds as obstacles
84        for component in layout.get_placed_components(primitives) {
85            // Shrink bounds slightly to allow wires to touch pins
86            let margin = Coord::from_mils(5.0);
87            self.obstacles.push(CoordRect::from_points(
88                component.bounds.location1.x + margin,
89                component.bounds.location1.y + margin,
90                component.bounds.location2.x - margin,
91                component.bounds.location2.y - margin,
92            ));
93        }
94
95        // Add existing wires
96        for record in primitives {
97            if let SchRecord::Wire(wire) = record {
98                for i in 0..wire.vertices.len().saturating_sub(1) {
99                    let start = CoordPoint::from_raw(wire.vertices[i].0, wire.vertices[i].1);
100                    let end = CoordPoint::from_raw(wire.vertices[i + 1].0, wire.vertices[i + 1].1);
101                    self.existing_wires.push(WireSegment::new(start, end));
102                }
103            }
104        }
105    }
106
107    /// Route a wire between two points using A* pathfinding.
108    pub fn route(&self, start: CoordPoint, end: CoordPoint) -> Option<RoutingPath> {
109        let start = self.grid.snap(start);
110        let end = self.grid.snap(end);
111
112        // Quick check for direct path
113        if let Some(path) = self.try_direct_path(start, end) {
114            return Some(path);
115        }
116
117        // Try L-shaped paths (two segments)
118        if let Some(path) = self.try_l_path(start, end) {
119            return Some(path);
120        }
121
122        // Fall back to A* for complex routes
123        self.route_astar(start, end)
124    }
125
126    /// Try to find a direct horizontal or vertical path.
127    fn try_direct_path(&self, start: CoordPoint, end: CoordPoint) -> Option<RoutingPath> {
128        let segment = WireSegment::new(start, end);
129
130        // Only works for axis-aligned paths
131        if !segment.is_horizontal() && !segment.is_vertical() {
132            return None;
133        }
134
135        // Check for obstacles
136        if self.segment_blocked(&segment) {
137            return None;
138        }
139
140        let mut path = RoutingPath::new();
141        path.add_segment(segment);
142        Some(path)
143    }
144
145    /// Try to find an L-shaped path (horizontal then vertical, or vice versa).
146    fn try_l_path(&self, start: CoordPoint, end: CoordPoint) -> Option<RoutingPath> {
147        // Try horizontal-first L
148        let corner1 = CoordPoint::new(end.x, start.y);
149        let seg1_h = WireSegment::new(start, corner1);
150        let seg2_v = WireSegment::new(corner1, end);
151
152        if !self.segment_blocked(&seg1_h) && !self.segment_blocked(&seg2_v) {
153            let mut path = RoutingPath::new();
154            path.add_segment(seg1_h);
155            path.add_segment(seg2_v);
156            return Some(path);
157        }
158
159        // Try vertical-first L
160        let corner2 = CoordPoint::new(start.x, end.y);
161        let seg1_v = WireSegment::new(start, corner2);
162        let seg2_h = WireSegment::new(corner2, end);
163
164        if !self.segment_blocked(&seg1_v) && !self.segment_blocked(&seg2_h) {
165            let mut path = RoutingPath::new();
166            path.add_segment(seg1_v);
167            path.add_segment(seg2_h);
168            return Some(path);
169        }
170
171        None
172    }
173
174    /// Check if a segment is blocked by an obstacle.
175    fn segment_blocked(&self, segment: &WireSegment) -> bool {
176        let bounds = segment.bounds();
177
178        for obstacle in &self.obstacles {
179            if bounds.intersects(*obstacle) {
180                // More precise check for the line segment
181                if self.line_intersects_rect(segment, obstacle) {
182                    return true;
183                }
184            }
185        }
186
187        false
188    }
189
190    /// Check if a line segment intersects a rectangle.
191    fn line_intersects_rect(&self, segment: &WireSegment, rect: &CoordRect) -> bool {
192        let (x1, y1) = (segment.start.x.to_raw(), segment.start.y.to_raw());
193        let (x2, y2) = (segment.end.x.to_raw(), segment.end.y.to_raw());
194        let (rx1, ry1) = (rect.location1.x.to_raw(), rect.location1.y.to_raw());
195        let (rx2, ry2) = (rect.location2.x.to_raw(), rect.location2.y.to_raw());
196
197        // For horizontal/vertical segments, use simple range check
198        if segment.is_horizontal() {
199            let y = y1;
200            if y < ry1 || y > ry2 {
201                return false;
202            }
203            let (min_x, max_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
204            return max_x > rx1 && min_x < rx2;
205        }
206
207        if segment.is_vertical() {
208            let x = x1;
209            if x < rx1 || x > rx2 {
210                return false;
211            }
212            let (min_y, max_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
213            return max_y > ry1 && min_y < ry2;
214        }
215
216        // For diagonal segments (shouldn't happen often in schematics)
217        // Use Cohen-Sutherland style region checking
218        false
219    }
220
221    /// Route using A* algorithm.
222    fn route_astar(&self, start: CoordPoint, end: CoordPoint) -> Option<RoutingPath> {
223        let mut open_set = BinaryHeap::new();
224        let mut came_from: HashMap<(i32, i32), ((i32, i32), Direction)> = HashMap::new();
225        let mut g_score: HashMap<(i32, i32), i64> = HashMap::new();
226        let mut closed_set: HashSet<(i32, i32)> = HashSet::new();
227
228        let start_key = (start.x.to_raw(), start.y.to_raw());
229        let end_key = (end.x.to_raw(), end.y.to_raw());
230
231        g_score.insert(start_key, 0);
232
233        open_set.push(PathNode {
234            position: start,
235            g_cost: 0,
236            f_cost: self.heuristic(start, end),
237            direction: None,
238        });
239
240        let grid_step = self.grid.spacing.to_raw();
241        let directions = [
242            (Direction::Right, (grid_step, 0)),
243            (Direction::Left, (-grid_step, 0)),
244            (Direction::Up, (0, grid_step)),
245            (Direction::Down, (0, -grid_step)),
246        ];
247
248        let mut iterations = 0;
249
250        while let Some(current) = open_set.pop() {
251            iterations += 1;
252            if iterations > self.max_iterations {
253                return None; // Give up if taking too long
254            }
255
256            let current_key = (current.position.x.to_raw(), current.position.y.to_raw());
257
258            if current_key == end_key {
259                return Some(self.reconstruct_path(came_from, start_key, end_key));
260            }
261
262            if closed_set.contains(&current_key) {
263                continue;
264            }
265            closed_set.insert(current_key);
266
267            for (dir, (dx, dy)) in &directions {
268                let next_pos = CoordPoint::from_raw(
269                    current.position.x.to_raw() + dx,
270                    current.position.y.to_raw() + dy,
271                );
272                let next_key = (next_pos.x.to_raw(), next_pos.y.to_raw());
273
274                if closed_set.contains(&next_key) {
275                    continue;
276                }
277
278                // Check if this position is blocked
279                if self.point_blocked(next_pos) && next_key != end_key {
280                    continue;
281                }
282
283                // Calculate cost
284                let mut move_cost = STRAIGHT_COST;
285                if let Some(prev_dir) = current.direction {
286                    if prev_dir != *dir {
287                        move_cost += TURN_COST;
288                    }
289                }
290
291                // Check for wire crossings
292                let test_segment = WireSegment::new(current.position, next_pos);
293                if self.crosses_wire(&test_segment) {
294                    move_cost += CROSSING_COST;
295                }
296
297                let tentative_g = current.g_cost + move_cost;
298
299                if tentative_g < *g_score.get(&next_key).unwrap_or(&i64::MAX) {
300                    came_from.insert(next_key, (current_key, *dir));
301                    g_score.insert(next_key, tentative_g);
302
303                    let f_cost = tentative_g + self.heuristic(next_pos, end);
304                    open_set.push(PathNode {
305                        position: next_pos,
306                        g_cost: tentative_g,
307                        f_cost,
308                        direction: Some(*dir),
309                    });
310                }
311            }
312        }
313
314        None // No path found
315    }
316
317    /// Manhattan distance heuristic.
318    fn heuristic(&self, from: CoordPoint, to: CoordPoint) -> i64 {
319        let dx = (to.x.to_raw() - from.x.to_raw()).abs() as i64;
320        let dy = (to.y.to_raw() - from.y.to_raw()).abs() as i64;
321        dx + dy
322    }
323
324    /// Check if a point is inside an obstacle.
325    fn point_blocked(&self, point: CoordPoint) -> bool {
326        for obstacle in &self.obstacles {
327            if obstacle.contains(point) {
328                return true;
329            }
330        }
331        false
332    }
333
334    /// Check if a segment crosses an existing wire.
335    fn crosses_wire(&self, segment: &WireSegment) -> bool {
336        for wire in &self.existing_wires {
337            if self.segments_intersect(segment, wire) {
338                return true;
339            }
340        }
341        false
342    }
343
344    /// Check if two segments intersect (not just touch at endpoints).
345    fn segments_intersect(&self, s1: &WireSegment, s2: &WireSegment) -> bool {
346        // Only care about perpendicular crossings
347        if (s1.is_horizontal() && s2.is_horizontal()) || (s1.is_vertical() && s2.is_vertical()) {
348            return false;
349        }
350
351        let (h_seg, v_seg) = if s1.is_horizontal() {
352            (s1, s2)
353        } else if s1.is_vertical() && s2.is_horizontal() {
354            (s2, s1)
355        } else {
356            return false;
357        };
358
359        let h_y = h_seg.start.y.to_raw();
360        let h_x1 = h_seg.start.x.to_raw().min(h_seg.end.x.to_raw());
361        let h_x2 = h_seg.start.x.to_raw().max(h_seg.end.x.to_raw());
362
363        let v_x = v_seg.start.x.to_raw();
364        let v_y1 = v_seg.start.y.to_raw().min(v_seg.end.y.to_raw());
365        let v_y2 = v_seg.start.y.to_raw().max(v_seg.end.y.to_raw());
366
367        // Check if they cross (not at endpoints)
368        v_x > h_x1 && v_x < h_x2 && h_y > v_y1 && h_y < v_y2
369    }
370
371    /// Reconstruct path from A* came_from map.
372    fn reconstruct_path(
373        &self,
374        came_from: HashMap<(i32, i32), ((i32, i32), Direction)>,
375        start: (i32, i32),
376        end: (i32, i32),
377    ) -> RoutingPath {
378        let mut path = RoutingPath::new();
379        let mut vertices = vec![CoordPoint::from_raw(end.0, end.1)];
380        let mut current = end;
381
382        while current != start {
383            if let Some((prev, _dir)) = came_from.get(&current) {
384                vertices.push(CoordPoint::from_raw(prev.0, prev.1));
385                current = *prev;
386            } else {
387                break;
388            }
389        }
390
391        vertices.reverse();
392
393        // Simplify path by merging collinear segments
394        let simplified = self.simplify_vertices(&vertices);
395
396        for i in 0..simplified.len().saturating_sub(1) {
397            path.add_segment(WireSegment::new(simplified[i], simplified[i + 1]));
398        }
399
400        path
401    }
402
403    /// Simplify vertices by removing collinear intermediate points.
404    fn simplify_vertices(&self, vertices: &[CoordPoint]) -> Vec<CoordPoint> {
405        if vertices.len() <= 2 {
406            return vertices.to_vec();
407        }
408
409        let mut simplified = vec![vertices[0]];
410
411        for i in 1..vertices.len() - 1 {
412            let prev = simplified.last().unwrap();
413            let curr = &vertices[i];
414            let next = &vertices[i + 1];
415
416            // Check if curr is collinear with prev and next
417            let dx1 = curr.x.to_raw() - prev.x.to_raw();
418            let dy1 = curr.y.to_raw() - prev.y.to_raw();
419            let dx2 = next.x.to_raw() - curr.x.to_raw();
420            let dy2 = next.y.to_raw() - curr.y.to_raw();
421
422            // Not collinear if direction changes
423            let collinear = (dx1 == 0 && dx2 == 0) || (dy1 == 0 && dy2 == 0);
424            if !collinear {
425                simplified.push(*curr);
426            }
427        }
428
429        simplified.push(*vertices.last().unwrap());
430        simplified
431    }
432
433    /// Find junctions where new wires intersect existing wires.
434    pub fn find_junctions(&self, path: &RoutingPath) -> Vec<CoordPoint> {
435        let mut junctions = Vec::new();
436
437        for segment in &path.segments {
438            for wire in &self.existing_wires {
439                if let Some(intersection) = self.find_intersection(segment, wire) {
440                    // Don't add junction at segment endpoints
441                    if intersection != segment.start && intersection != segment.end {
442                        junctions.push(intersection);
443                    }
444                }
445            }
446        }
447
448        // Remove duplicates
449        junctions.sort_by_key(|p| (p.x.to_raw(), p.y.to_raw()));
450        junctions.dedup_by(|a, b| a.x == b.x && a.y == b.y);
451
452        junctions
453    }
454
455    /// Find intersection point of two segments.
456    fn find_intersection(&self, s1: &WireSegment, s2: &WireSegment) -> Option<CoordPoint> {
457        // Only care about perpendicular intersections
458        if (s1.is_horizontal() && s2.is_horizontal()) || (s1.is_vertical() && s2.is_vertical()) {
459            return None;
460        }
461
462        let (h_seg, v_seg) = if s1.is_horizontal() {
463            (s1, s2)
464        } else if s1.is_vertical() && s2.is_horizontal() {
465            (s2, s1)
466        } else {
467            return None;
468        };
469
470        let h_y = h_seg.start.y;
471        let h_x1 = h_seg.start.x.min(h_seg.end.x);
472        let h_x2 = h_seg.start.x.max(h_seg.end.x);
473
474        let v_x = v_seg.start.x;
475        let v_y1 = v_seg.start.y.min(v_seg.end.y);
476        let v_y2 = v_seg.start.y.max(v_seg.end.y);
477
478        // Check if they intersect
479        if v_x >= h_x1 && v_x <= h_x2 && h_y >= v_y1 && h_y <= v_y2 {
480            Some(CoordPoint::new(v_x, h_y))
481        } else {
482            None
483        }
484    }
485
486    /// Auto-route multiple connections, trying to minimize crossings.
487    pub fn auto_route_all(
488        &mut self,
489        connections: &[(CoordPoint, CoordPoint)],
490    ) -> Vec<Option<RoutingPath>> {
491        let mut results = Vec::new();
492
493        // Sort connections by distance (shorter first)
494        let mut sorted_connections: Vec<_> = connections.iter().enumerate().collect();
495        sorted_connections.sort_by_key(|(_, (start, end))| {
496            let dx = (end.x - start.x).abs().to_raw();
497            let dy = (end.y - start.y).abs().to_raw();
498            dx + dy
499        });
500
501        for (_, (start, end)) in sorted_connections {
502            let path = self.route(*start, *end);
503
504            // Add successful route as obstacle for future routes
505            if let Some(ref p) = path {
506                for segment in &p.segments {
507                    self.existing_wires.push(*segment);
508                }
509            }
510
511            results.push(path);
512        }
513
514        results
515    }
516}