Skip to main content

altium_format/edit/
netlist.rs

1//! Netlist builder for analyzing and managing net connectivity.
2
3use std::collections::{HashMap, HashSet};
4
5use crate::records::sch::{SchPin, SchRecord};
6use crate::types::Coord;
7
8/// A connection point on the schematic.
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct ConnectionPoint {
11    /// Location of the connection.
12    pub location: (i32, i32),
13    /// Type of connection.
14    pub kind: ConnectionKind,
15}
16
17/// Types of connection points.
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub enum ConnectionKind {
20    /// Pin connection with component designator and pin designator.
21    Pin { component: String, pin: String },
22    /// Wire endpoint.
23    WireEnd { wire_index: usize },
24    /// Junction.
25    Junction { index: usize },
26    /// Net label.
27    NetLabel { name: String },
28    /// Power port.
29    PowerPort { name: String },
30    /// Sheet port.
31    Port { name: String },
32}
33
34/// A net connecting multiple points.
35#[derive(Debug, Clone)]
36pub struct Net {
37    /// Net name (may be auto-generated).
38    pub name: String,
39    /// Whether the name was explicitly assigned (via net label).
40    pub named: bool,
41    /// All connection points in this net.
42    pub connections: Vec<ConnectionPoint>,
43}
44
45impl Net {
46    /// Get all pin connections in this net.
47    pub fn pins(&self) -> Vec<(&str, &str)> {
48        self.connections
49            .iter()
50            .filter_map(|c| match &c.kind {
51                ConnectionKind::Pin { component, pin } => Some((component.as_str(), pin.as_str())),
52                _ => None,
53            })
54            .collect()
55    }
56
57    /// Check if this net connects to a specific component.
58    pub fn connects_to(&self, component: &str) -> bool {
59        self.connections.iter().any(|c| match &c.kind {
60            ConnectionKind::Pin { component: c, .. } => c == component,
61            _ => false,
62        })
63    }
64}
65
66/// Netlist containing all nets in a schematic.
67#[derive(Debug, Clone, Default)]
68pub struct Netlist {
69    /// All nets.
70    pub nets: Vec<Net>,
71    /// Map from location to net index.
72    location_to_net: HashMap<(i32, i32), usize>,
73}
74
75impl Netlist {
76    /// Get a net by name.
77    pub fn get_by_name(&self, name: &str) -> Option<&Net> {
78        self.nets.iter().find(|n| n.name == name)
79    }
80
81    /// Get all nets connected to a component.
82    pub fn nets_for_component(&self, designator: &str) -> Vec<&Net> {
83        self.nets
84            .iter()
85            .filter(|n| n.connects_to(designator))
86            .collect()
87    }
88
89    /// Get the net at a specific location.
90    pub fn net_at(&self, x: i32, y: i32) -> Option<&Net> {
91        self.location_to_net.get(&(x, y)).map(|&i| &self.nets[i])
92    }
93}
94
95/// Builder for constructing netlists from schematic primitives.
96pub struct NetlistBuilder {
97    /// Tolerance for matching connection points (in internal units).
98    tolerance: i32,
99}
100
101impl Default for NetlistBuilder {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107impl NetlistBuilder {
108    /// Create a new netlist builder.
109    pub fn new() -> Self {
110        Self {
111            tolerance: Coord::from_mils(1.0).to_raw(), // 1 mil tolerance
112        }
113    }
114
115    /// Set the tolerance for matching connection points.
116    pub fn with_tolerance(mut self, mils: f64) -> Self {
117        self.tolerance = Coord::from_mils(mils).to_raw();
118        self
119    }
120
121    /// Build a netlist from schematic primitives.
122    pub fn build(&self, primitives: &[SchRecord]) -> Netlist {
123        // Step 1: Collect all connection points
124        let mut points: Vec<ConnectionPoint> = Vec::new();
125        let mut designators: HashMap<usize, String> = HashMap::new();
126
127        // First pass: collect designators
128        for record in primitives.iter() {
129            if let SchRecord::Designator(d) = record {
130                let owner = d.param.label.graphical.base.owner_index as usize;
131                designators.insert(owner, d.text().to_string());
132            }
133        }
134
135        // Second pass: collect connection points
136        for (i, record) in primitives.iter().enumerate() {
137            match record {
138                SchRecord::Pin(pin) => {
139                    let owner = pin.graphical.base.owner_index as usize;
140                    let component = designators.get(&owner).cloned().unwrap_or_default();
141                    let endpoint = self.get_pin_endpoint(pin);
142
143                    points.push(ConnectionPoint {
144                        location: endpoint,
145                        kind: ConnectionKind::Pin {
146                            component,
147                            pin: pin.designator.clone(),
148                        },
149                    });
150                }
151                SchRecord::Wire(wire) => {
152                    // Add both endpoints
153                    for (j, vertex) in wire.vertices.iter().enumerate() {
154                        if j == 0 || j == wire.vertices.len() - 1 {
155                            points.push(ConnectionPoint {
156                                location: *vertex,
157                                kind: ConnectionKind::WireEnd { wire_index: i },
158                            });
159                        }
160                    }
161                }
162                SchRecord::Junction(junction) => {
163                    points.push(ConnectionPoint {
164                        location: (junction.graphical.location_x, junction.graphical.location_y),
165                        kind: ConnectionKind::Junction { index: i },
166                    });
167                }
168                SchRecord::NetLabel(label) => {
169                    points.push(ConnectionPoint {
170                        location: (
171                            label.label.graphical.location_x,
172                            label.label.graphical.location_y,
173                        ),
174                        kind: ConnectionKind::NetLabel {
175                            name: label.label.text.clone(),
176                        },
177                    });
178                }
179                SchRecord::PowerObject(power) => {
180                    points.push(ConnectionPoint {
181                        location: (power.graphical.location_x, power.graphical.location_y),
182                        kind: ConnectionKind::PowerPort {
183                            name: power.text.clone(),
184                        },
185                    });
186                }
187                SchRecord::Port(port) => {
188                    points.push(ConnectionPoint {
189                        location: (port.graphical.location_x, port.graphical.location_y),
190                        kind: ConnectionKind::Port {
191                            name: port.name.clone(),
192                        },
193                    });
194                }
195                _ => {}
196            }
197        }
198
199        // Step 2: Collect wire segments for connectivity
200        let mut wire_segments: Vec<((i32, i32), (i32, i32))> = Vec::new();
201        for record in primitives {
202            if let SchRecord::Wire(wire) = record {
203                for i in 0..wire.vertices.len().saturating_sub(1) {
204                    wire_segments.push((wire.vertices[i], wire.vertices[i + 1]));
205                }
206            }
207        }
208
209        // Step 3: Build connectivity graph using union-find
210        let mut union_find = UnionFind::new(points.len());
211
212        // Connect points at the same location
213        let mut location_map: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
214        for (i, point) in points.iter().enumerate() {
215            // Snap to grid for matching
216            let snapped = self.snap_location(point.location);
217            location_map.entry(snapped).or_default().push(i);
218        }
219
220        for indices in location_map.values() {
221            for i in 1..indices.len() {
222                union_find.union(indices[0], indices[i]);
223            }
224        }
225
226        // Connect points along wire segments
227        for (start, end) in &wire_segments {
228            let snapped_start = self.snap_location(*start);
229            let snapped_end = self.snap_location(*end);
230
231            if let (Some(start_indices), Some(end_indices)) = (
232                location_map.get(&snapped_start),
233                location_map.get(&snapped_end),
234            ) {
235                if !start_indices.is_empty() && !end_indices.is_empty() {
236                    union_find.union(start_indices[0], end_indices[0]);
237                }
238            }
239
240            // Also check for points along the wire segment
241            for (loc, indices) in &location_map {
242                if self.point_on_segment(*loc, *start, *end) && !indices.is_empty() {
243                    if let Some(start_idx) =
244                        location_map.get(&snapped_start).and_then(|v| v.first())
245                    {
246                        union_find.union(*start_idx, indices[0]);
247                    }
248                }
249            }
250        }
251
252        // Step 4: Group points into nets
253        let mut net_groups: HashMap<usize, Vec<usize>> = HashMap::new();
254        for i in 0..points.len() {
255            let root = union_find.find(i);
256            net_groups.entry(root).or_default().push(i);
257        }
258
259        // Step 5: Create nets
260        let mut nets = Vec::new();
261        let mut net_counter = 1;
262
263        for (_, group) in net_groups {
264            let connections: Vec<ConnectionPoint> =
265                group.iter().map(|&i| points[i].clone()).collect();
266
267            // Find net name
268            let (name, named) = self.find_net_name(&connections, &mut net_counter);
269
270            nets.push(Net {
271                name,
272                named,
273                connections,
274            });
275        }
276
277        // Sort nets by name for consistent output
278        nets.sort_by(|a, b| a.name.cmp(&b.name));
279
280        // Build location-to-net map
281        let mut location_to_net = HashMap::new();
282        for (i, net) in nets.iter().enumerate() {
283            for conn in &net.connections {
284                location_to_net.insert(conn.location, i);
285            }
286        }
287
288        Netlist {
289            nets,
290            location_to_net,
291        }
292    }
293
294    /// Snap a location to tolerance grid for matching.
295    fn snap_location(&self, loc: (i32, i32)) -> (i32, i32) {
296        let tol = self.tolerance.max(1);
297        ((loc.0 / tol) * tol, (loc.1 / tol) * tol)
298    }
299
300    /// Get the endpoint of a pin (where wires connect).
301    fn get_pin_endpoint(&self, pin: &SchPin) -> (i32, i32) {
302        use crate::records::sch::PinConglomerateFlags;
303
304        let base_x = pin.graphical.location_x;
305        let base_y = pin.graphical.location_y;
306        let length = pin.pin_length;
307
308        let rotated = pin.pin_conglomerate.contains(PinConglomerateFlags::ROTATED);
309        let flipped = pin.pin_conglomerate.contains(PinConglomerateFlags::FLIPPED);
310
311        let (dx, dy) = match (rotated, flipped) {
312            (false, false) => (length, 0),
313            (true, false) => (0, length),
314            (false, true) => (-length, 0),
315            (true, true) => (0, -length),
316        };
317
318        (base_x + dx, base_y + dy)
319    }
320
321    /// Check if a point lies on a wire segment.
322    fn point_on_segment(&self, point: (i32, i32), start: (i32, i32), end: (i32, i32)) -> bool {
323        let (px, py) = point;
324        let (sx, sy) = start;
325        let (ex, ey) = end;
326
327        // Check if point is within bounding box
328        let min_x = sx.min(ex) - self.tolerance;
329        let max_x = sx.max(ex) + self.tolerance;
330        let min_y = sy.min(ey) - self.tolerance;
331        let max_y = sy.max(ey) + self.tolerance;
332
333        if px < min_x || px > max_x || py < min_y || py > max_y {
334            return false;
335        }
336
337        // For horizontal segment
338        if sy == ey && (py - sy).abs() <= self.tolerance {
339            return true;
340        }
341
342        // For vertical segment
343        if sx == ex && (px - sx).abs() <= self.tolerance {
344            return true;
345        }
346
347        false
348    }
349
350    /// Find the net name from connections.
351    fn find_net_name(&self, connections: &[ConnectionPoint], counter: &mut u32) -> (String, bool) {
352        // Priority: Net label > Power port > Port > Auto-generated
353
354        // Check for net label
355        for conn in connections {
356            if let ConnectionKind::NetLabel { name } = &conn.kind {
357                if !name.is_empty() {
358                    return (name.clone(), true);
359                }
360            }
361        }
362
363        // Check for power port
364        for conn in connections {
365            if let ConnectionKind::PowerPort { name } = &conn.kind {
366                if !name.is_empty() {
367                    return (name.clone(), true);
368                }
369            }
370        }
371
372        // Check for port
373        for conn in connections {
374            if let ConnectionKind::Port { name } = &conn.kind {
375                if !name.is_empty() {
376                    return (name.clone(), true);
377                }
378            }
379        }
380
381        // Auto-generate
382        let name = format!("Net{}", counter);
383        *counter += 1;
384        (name, false)
385    }
386
387    /// Find unconnected pins in the schematic.
388    pub fn find_unconnected_pins(
389        &self,
390        primitives: &[SchRecord],
391    ) -> Vec<(String, String, (i32, i32))> {
392        let netlist = self.build(primitives);
393        let mut unconnected = Vec::new();
394
395        // Find nets with only a single pin connection
396        for net in &netlist.nets {
397            let pins: Vec<_> = net
398                .connections
399                .iter()
400                .filter(|c| matches!(c.kind, ConnectionKind::Pin { .. }))
401                .collect();
402
403            if pins.len() == 1 {
404                // Check if net has any wire connections
405                let has_wires = net
406                    .connections
407                    .iter()
408                    .any(|c| matches!(c.kind, ConnectionKind::WireEnd { .. }));
409
410                if !has_wires {
411                    if let ConnectionKind::Pin { component, pin } = &pins[0].kind {
412                        unconnected.push((component.clone(), pin.clone(), pins[0].location));
413                    }
414                }
415            }
416        }
417
418        unconnected
419    }
420
421    /// Find locations where junctions might be needed.
422    pub fn find_missing_junctions(&self, primitives: &[SchRecord]) -> Vec<(i32, i32)> {
423        let mut wire_points: HashMap<(i32, i32), usize> = HashMap::new();
424        let mut junction_locations: HashSet<(i32, i32)> = HashSet::new();
425
426        // Collect existing junction locations
427        for record in primitives {
428            if let SchRecord::Junction(j) = record {
429                junction_locations.insert((j.graphical.location_x, j.graphical.location_y));
430            }
431        }
432
433        // Count wire connections at each point
434        for record in primitives {
435            if let SchRecord::Wire(wire) = record {
436                for i in 0..wire.vertices.len().saturating_sub(1) {
437                    let start = wire.vertices[i];
438                    let end = wire.vertices[i + 1];
439
440                    // Count endpoints
441                    *wire_points.entry(start).or_insert(0) += 1;
442                    *wire_points.entry(end).or_insert(0) += 1;
443
444                    // Check for T-junctions (wire crossing another wire's midpoint)
445                    for other_record in primitives {
446                        if let SchRecord::Wire(other_wire) = other_record {
447                            for j in 0..other_wire.vertices.len().saturating_sub(1) {
448                                let other_start = other_wire.vertices[j];
449                                let other_end = other_wire.vertices[j + 1];
450
451                                // Check if our wire's endpoint lies on another wire's segment
452                                if self.point_on_segment(start, other_start, other_end)
453                                    && start != other_start
454                                    && start != other_end
455                                {
456                                    *wire_points.entry(start).or_insert(0) += 1;
457                                }
458                                if self.point_on_segment(end, other_start, other_end)
459                                    && end != other_start
460                                    && end != other_end
461                                {
462                                    *wire_points.entry(end).or_insert(0) += 1;
463                                }
464                            }
465                        }
466                    }
467                }
468            }
469        }
470
471        // Find points with 3+ connections that don't have junctions
472        let mut missing = Vec::new();
473        for (loc, count) in wire_points {
474            if count >= 3 && !junction_locations.contains(&loc) {
475                missing.push(loc);
476            }
477        }
478
479        missing
480    }
481}
482
483/// Union-Find data structure for connectivity.
484struct UnionFind {
485    parent: Vec<usize>,
486    rank: Vec<usize>,
487}
488
489impl UnionFind {
490    fn new(size: usize) -> Self {
491        Self {
492            parent: (0..size).collect(),
493            rank: vec![0; size],
494        }
495    }
496
497    fn find(&mut self, x: usize) -> usize {
498        if self.parent[x] != x {
499            self.parent[x] = self.find(self.parent[x]);
500        }
501        self.parent[x]
502    }
503
504    fn union(&mut self, x: usize, y: usize) {
505        let root_x = self.find(x);
506        let root_y = self.find(y);
507
508        if root_x != root_y {
509            if self.rank[root_x] < self.rank[root_y] {
510                self.parent[root_x] = root_y;
511            } else if self.rank[root_x] > self.rank[root_y] {
512                self.parent[root_y] = root_x;
513            } else {
514                self.parent[root_y] = root_x;
515                self.rank[root_x] += 1;
516            }
517        }
518    }
519}