Skip to main content

altium_format/query/
view.rs

1//! High-level schematic view with connectivity analysis.
2//!
3//! This module provides a semantic representation of schematic documents,
4//! computing net connectivity from raw primitives.
5
6use crate::io::schdoc::SchDoc;
7use crate::records::sch::*;
8use crate::tree::RecordTree;
9use std::collections::{HashMap, HashSet};
10
11// Re-export ElectricalType from common module
12pub use super::common::ElectricalType;
13
14/// A connection point in the schematic
15#[derive(Debug, Clone, PartialEq)]
16pub enum ConnectionPoint {
17    /// Component pin
18    Pin {
19        component_designator: String,
20        pin_designator: String,
21        pin_name: String,
22    },
23    /// Inter-sheet port
24    Port { name: String, io_type: String },
25    /// Power/ground symbol
26    PowerRail { net_name: String, is_ground: bool },
27    /// Net label
28    NetLabel { name: String },
29}
30
31impl ConnectionPoint {
32    /// Get a short string representation
33    pub fn to_short_string(&self) -> String {
34        match self {
35            ConnectionPoint::Pin {
36                component_designator,
37                pin_designator,
38                pin_name,
39            } => {
40                if pin_name.is_empty() {
41                    format!("{}.{}", component_designator, pin_designator)
42                } else {
43                    format!("{}.{} ({})", component_designator, pin_designator, pin_name)
44                }
45            }
46            ConnectionPoint::Port { name, .. } => format!("PORT:{}", name),
47            ConnectionPoint::PowerRail {
48                net_name,
49                is_ground,
50            } => {
51                if *is_ground {
52                    format!("GND:{}", net_name)
53                } else {
54                    format!("PWR:{}", net_name)
55                }
56            }
57            ConnectionPoint::NetLabel { name } => format!("LABEL:{}", name),
58        }
59    }
60}
61
62/// High-level view of a component
63#[derive(Debug, Clone)]
64pub struct ComponentView {
65    /// Reference designator (e.g., "U1", "R1")
66    pub designator: String,
67    /// Library reference / part name
68    pub part_name: String,
69    /// Component description
70    pub description: String,
71    /// Value parameter if present
72    pub value: Option<String>,
73    /// Footprint name
74    pub footprint: Option<String>,
75    /// All pins on this component
76    pub pins: Vec<PinView>,
77    /// Additional parameters
78    pub parameters: HashMap<String, String>,
79    /// Record index in primitives list
80    pub record_index: usize,
81}
82
83/// High-level view of a pin
84#[derive(Debug, Clone)]
85pub struct PinView {
86    /// Pin designator/number
87    pub designator: String,
88    /// Pin name
89    pub name: String,
90    /// Electrical type
91    pub electrical_type: ElectricalType,
92    /// Net this pin connects to
93    pub connected_net: Option<String>,
94    /// Whether pin is hidden
95    pub is_hidden: bool,
96    /// Hidden net name (for power pins with implicit connection)
97    pub hidden_net: Option<String>,
98    /// Parent component designator
99    pub component_designator: String,
100    /// Location for connectivity analysis
101    pub location: (i32, i32),
102    /// Pin end location (corner)
103    pub corner: (i32, i32),
104}
105
106/// High-level view of a net
107#[derive(Debug, Clone)]
108pub struct NetView {
109    /// Net name
110    pub name: String,
111    /// Whether this is a power net
112    pub is_power: bool,
113    /// Whether this is a ground net
114    pub is_ground: bool,
115    /// All connection points on this net
116    pub connections: Vec<ConnectionPoint>,
117}
118
119/// High-level view of a port
120#[derive(Debug, Clone)]
121pub struct PortView {
122    /// Port name
123    pub name: String,
124    /// I/O type
125    pub io_type: String,
126    /// Harness type
127    pub harness: Option<String>,
128    /// Connected net
129    pub connected_net: Option<String>,
130    /// Location
131    pub location: (i32, i32),
132    /// Record index
133    pub record_index: usize,
134}
135
136/// High-level view of a power symbol
137#[derive(Debug, Clone)]
138pub struct PowerView {
139    /// Net name
140    pub net_name: String,
141    /// Power style
142    pub style: String,
143    /// Whether this is a ground symbol
144    pub is_ground: bool,
145    /// Location
146    pub location: (i32, i32),
147    /// Record index
148    pub record_index: usize,
149}
150
151/// Wire segment information
152#[derive(Debug, Clone)]
153pub struct WireView {
154    /// Vertices of the wire
155    pub vertices: Vec<(i32, i32)>,
156    /// Record index
157    pub record_index: usize,
158}
159
160/// Net label information
161#[derive(Debug, Clone)]
162pub struct LabelView {
163    /// Label text (net name)
164    pub text: String,
165    /// Location
166    pub location: (i32, i32),
167    /// Record index
168    pub record_index: usize,
169}
170
171/// Junction information
172#[derive(Debug, Clone)]
173pub struct JunctionView {
174    /// Location
175    pub location: (i32, i32),
176    /// Record index
177    pub record_index: usize,
178}
179
180/// High-level schematic document view with connectivity
181#[derive(Debug)]
182pub struct SchematicView {
183    /// Sheet name/title
184    pub sheet_name: Option<String>,
185    /// All components
186    pub components: Vec<ComponentView>,
187    /// All nets (computed from connectivity)
188    pub nets: Vec<NetView>,
189    /// All ports
190    pub ports: Vec<PortView>,
191    /// All power symbols
192    pub power_symbols: Vec<PowerView>,
193    /// All wires
194    pub wires: Vec<WireView>,
195    /// All net labels
196    pub labels: Vec<LabelView>,
197    /// All junctions
198    pub junctions: Vec<JunctionView>,
199    /// Component index by designator
200    component_index: HashMap<String, usize>,
201    /// Net index by name
202    net_index: HashMap<String, usize>,
203}
204
205impl SchematicView {
206    /// Build a schematic view from a SchDoc
207    pub fn from_schdoc(doc: &SchDoc) -> Self {
208        let mut builder = SchematicViewBuilder::new(doc);
209        builder.build()
210    }
211
212    /// Get component by designator
213    pub fn get_component(&self, designator: &str) -> Option<&ComponentView> {
214        self.component_index
215            .get(designator)
216            .map(|&i| &self.components[i])
217    }
218
219    /// Get net by name
220    pub fn get_net(&self, name: &str) -> Option<&NetView> {
221        self.net_index.get(name).map(|&i| &self.nets[i])
222    }
223
224    /// Get all power net names
225    pub fn power_nets(&self) -> Vec<&str> {
226        self.nets
227            .iter()
228            .filter(|n| n.is_power && !n.is_ground)
229            .map(|n| n.name.as_str())
230            .collect()
231    }
232
233    /// Get all ground net names
234    pub fn ground_nets(&self) -> Vec<&str> {
235        self.nets
236            .iter()
237            .filter(|n| n.is_ground)
238            .map(|n| n.name.as_str())
239            .collect()
240    }
241
242    /// Find components by part name pattern
243    pub fn find_components_by_part(&self, pattern: &str) -> Vec<&ComponentView> {
244        let pattern_lower = pattern.to_lowercase();
245        self.components
246            .iter()
247            .filter(|c| c.part_name.to_lowercase().contains(&pattern_lower))
248            .collect()
249    }
250
251    /// Get all pins for a component
252    pub fn get_pins(&self, designator: &str) -> Vec<&PinView> {
253        self.get_component(designator)
254            .map(|c| c.pins.iter().collect())
255            .unwrap_or_default()
256    }
257}
258
259/// Builder for SchematicView that handles connectivity analysis
260struct SchematicViewBuilder<'a> {
261    doc: &'a SchDoc,
262    #[allow(dead_code)] // Reserved for future hierarchical record queries
263    tree: RecordTree<SchRecord>,
264    /// Coordinate to connection points map
265    coord_map: HashMap<(i32, i32), Vec<CoordEntry>>,
266    /// Union-find for net connectivity
267    net_union: HashMap<(i32, i32), (i32, i32)>,
268    /// Net names at coordinates
269    net_names: HashMap<(i32, i32), String>,
270    /// Power/ground flags at coordinates
271    power_coords: HashSet<(i32, i32)>,
272    ground_coords: HashSet<(i32, i32)>,
273}
274
275#[derive(Debug, Clone)]
276enum CoordEntry {
277    PinEnd {
278        component_designator: String,
279        pin_designator: String,
280        pin_name: String,
281        #[allow(dead_code)] // Reserved for future ERC checks based on pin electrical types
282        electrical_type: ElectricalType,
283    },
284    WireVertex {
285        #[allow(dead_code)] // Reserved for future wire segment highlighting
286        wire_index: usize,
287    },
288    Junction,
289    NetLabel {
290        name: String,
291    },
292    Port {
293        name: String,
294        io_type: String,
295    },
296    Power {
297        name: String,
298        is_ground: bool,
299    },
300}
301
302impl<'a> SchematicViewBuilder<'a> {
303    fn new(doc: &'a SchDoc) -> Self {
304        let tree = RecordTree::from_records(doc.primitives.clone());
305        Self {
306            doc,
307            tree,
308            coord_map: HashMap::new(),
309            net_union: HashMap::new(),
310            net_names: HashMap::new(),
311            power_coords: HashSet::new(),
312            ground_coords: HashSet::new(),
313        }
314    }
315
316    fn build(&mut self) -> SchematicView {
317        // Extract components and pins
318        let (components, component_index) = self.extract_components();
319
320        // Extract other elements
321        let ports = self.extract_ports();
322        let power_symbols = self.extract_power_symbols();
323        let wires = self.extract_wires();
324        let labels = self.extract_labels();
325        let junctions = self.extract_junctions();
326
327        // Build coordinate map for connectivity
328        self.build_coord_map(
329            &components,
330            &ports,
331            &power_symbols,
332            &wires,
333            &labels,
334            &junctions,
335        );
336
337        // Trace connectivity through wires
338        self.trace_connectivity(&wires);
339
340        // Build nets from traced connectivity
341        let nets = self.build_nets(&components);
342        let net_index: HashMap<String, usize> = nets
343            .iter()
344            .enumerate()
345            .map(|(i, n)| (n.name.clone(), i))
346            .collect();
347
348        // Update pins with their connected net names
349        let mut components = components;
350        self.update_pin_connections(&mut components, &nets);
351
352        SchematicView {
353            sheet_name: self.extract_sheet_name(),
354            components,
355            nets,
356            ports,
357            power_symbols,
358            wires,
359            labels,
360            junctions,
361            component_index,
362            net_index,
363        }
364    }
365
366    fn extract_components(&self) -> (Vec<ComponentView>, HashMap<String, usize>) {
367        let mut components = Vec::new();
368        let mut component_index = HashMap::new();
369
370        for (idx, record) in self.doc.primitives.iter().enumerate() {
371            if let SchRecord::Component(comp) = record {
372                // Find designator and parameters
373                let mut designator = String::new();
374                let mut value = None;
375                let mut footprint = None;
376                let mut parameters = HashMap::new();
377                let mut pins = Vec::new();
378
379                // Scan children for parameters and pins
380                for child in self.doc.primitives.iter() {
381                    let owner = child.owner_index();
382                    if owner == idx as i32 {
383                        match child {
384                            SchRecord::Designator(des) => {
385                                designator = des.param.label.text.clone();
386                            }
387                            SchRecord::Parameter(param) => {
388                                let name = param.name.to_uppercase();
389                                let val = param.label.text.clone();
390                                if name == "VALUE" {
391                                    value = Some(val.clone());
392                                }
393                                parameters.insert(param.name.clone(), val);
394                            }
395                            SchRecord::Pin(pin) => {
396                                let corner = pin.get_corner();
397                                pins.push(PinView {
398                                    designator: pin.designator.clone(),
399                                    name: pin.name.clone(),
400                                    electrical_type: ElectricalType::from_pin_electrical(
401                                        pin.electrical,
402                                    ),
403                                    connected_net: None,
404                                    is_hidden: pin.is_hidden(),
405                                    hidden_net: if pin.hidden_net_name.is_empty() {
406                                        None
407                                    } else {
408                                        Some(pin.hidden_net_name.clone())
409                                    },
410                                    component_designator: String::new(), // Set later
411                                    location: (pin.graphical.location_x, pin.graphical.location_y),
412                                    corner,
413                                });
414                            }
415                            SchRecord::Implementation(impl_rec) => {
416                                if impl_rec.model_type.to_uppercase() == "PCBLIB"
417                                    && impl_rec.is_current
418                                {
419                                    footprint = Some(impl_rec.model_name.clone());
420                                }
421                            }
422                            _ => {}
423                        }
424                    }
425                }
426
427                // Use lib_reference if designator not found
428                if designator.is_empty() {
429                    designator = format!("?{}", components.len());
430                }
431
432                // Update pin component designators
433                for pin in &mut pins {
434                    pin.component_designator = designator.clone();
435                }
436
437                let comp_view = ComponentView {
438                    designator: designator.clone(),
439                    part_name: comp.lib_reference.clone(),
440                    description: comp.component_description.clone(),
441                    value,
442                    footprint,
443                    pins,
444                    parameters,
445                    record_index: idx,
446                };
447
448                component_index.insert(designator.clone(), components.len());
449                components.push(comp_view);
450            }
451        }
452
453        (components, component_index)
454    }
455
456    fn extract_ports(&self) -> Vec<PortView> {
457        self.doc
458            .primitives
459            .iter()
460            .enumerate()
461            .filter_map(|(idx, record)| {
462                if let SchRecord::Port(port) = record {
463                    Some(PortView {
464                        name: port.name.clone(),
465                        io_type: format!("{:?}", port.io_type),
466                        harness: if port.harness_type.is_empty() {
467                            None
468                        } else {
469                            Some(port.harness_type.clone())
470                        },
471                        connected_net: None,
472                        location: (port.graphical.location_x, port.graphical.location_y),
473                        record_index: idx,
474                    })
475                } else {
476                    None
477                }
478            })
479            .collect()
480    }
481
482    fn extract_power_symbols(&self) -> Vec<PowerView> {
483        self.doc
484            .primitives
485            .iter()
486            .enumerate()
487            .filter_map(|(idx, record)| {
488                if let SchRecord::PowerObject(pwr) = record {
489                    let is_ground = matches!(
490                        pwr.style,
491                        PowerObjectStyle::Ground
492                            | PowerObjectStyle::SignalGround
493                            | PowerObjectStyle::EarthGround
494                            | PowerObjectStyle::PowerGround
495                    );
496                    Some(PowerView {
497                        net_name: pwr.text.clone(),
498                        style: format!("{:?}", pwr.style),
499                        is_ground,
500                        location: (pwr.graphical.location_x, pwr.graphical.location_y),
501                        record_index: idx,
502                    })
503                } else {
504                    None
505                }
506            })
507            .collect()
508    }
509
510    fn extract_wires(&self) -> Vec<WireView> {
511        self.doc
512            .primitives
513            .iter()
514            .enumerate()
515            .filter_map(|(idx, record)| {
516                if let SchRecord::Wire(wire) = record {
517                    Some(WireView {
518                        vertices: wire.vertices.clone(),
519                        record_index: idx,
520                    })
521                } else {
522                    None
523                }
524            })
525            .collect()
526    }
527
528    fn extract_labels(&self) -> Vec<LabelView> {
529        self.doc
530            .primitives
531            .iter()
532            .enumerate()
533            .filter_map(|(idx, record)| {
534                if let SchRecord::NetLabel(label) = record {
535                    Some(LabelView {
536                        text: label.label.text.clone(),
537                        location: (
538                            label.label.graphical.location_x,
539                            label.label.graphical.location_y,
540                        ),
541                        record_index: idx,
542                    })
543                } else {
544                    None
545                }
546            })
547            .collect()
548    }
549
550    fn extract_junctions(&self) -> Vec<JunctionView> {
551        self.doc
552            .primitives
553            .iter()
554            .enumerate()
555            .filter_map(|(idx, record)| {
556                if let SchRecord::Junction(junc) = record {
557                    Some(JunctionView {
558                        location: (junc.graphical.location_x, junc.graphical.location_y),
559                        record_index: idx,
560                    })
561                } else {
562                    None
563                }
564            })
565            .collect()
566    }
567
568    fn extract_sheet_name(&self) -> Option<String> {
569        // Try to find sheet header or use filename
570        for record in &self.doc.primitives {
571            if let SchRecord::SheetHeader(_) = record {
572                // Sheet header doesn't have name, would need to get from file
573                return None;
574            }
575        }
576        None
577    }
578
579    fn build_coord_map(
580        &mut self,
581        components: &[ComponentView],
582        ports: &[PortView],
583        power_symbols: &[PowerView],
584        wires: &[WireView],
585        labels: &[LabelView],
586        junctions: &[JunctionView],
587    ) {
588        // Add pin connection points (at pin corner, not base)
589        for comp in components {
590            for pin in &comp.pins {
591                let entry = CoordEntry::PinEnd {
592                    component_designator: comp.designator.clone(),
593                    pin_designator: pin.designator.clone(),
594                    pin_name: pin.name.clone(),
595                    electrical_type: pin.electrical_type,
596                };
597                self.coord_map.entry(pin.corner).or_default().push(entry);
598            }
599        }
600
601        // Add wire vertices
602        for (wire_idx, wire) in wires.iter().enumerate() {
603            for &vertex in &wire.vertices {
604                self.coord_map
605                    .entry(vertex)
606                    .or_default()
607                    .push(CoordEntry::WireVertex {
608                        wire_index: wire_idx,
609                    });
610            }
611        }
612
613        // Add junctions
614        for junc in junctions {
615            self.coord_map
616                .entry(junc.location)
617                .or_default()
618                .push(CoordEntry::Junction);
619        }
620
621        // Add net labels
622        for label in labels {
623            self.coord_map
624                .entry(label.location)
625                .or_default()
626                .push(CoordEntry::NetLabel {
627                    name: label.text.clone(),
628                });
629            self.net_names.insert(label.location, label.text.clone());
630        }
631
632        // Add ports
633        for port in ports {
634            self.coord_map
635                .entry(port.location)
636                .or_default()
637                .push(CoordEntry::Port {
638                    name: port.name.clone(),
639                    io_type: port.io_type.clone(),
640                });
641        }
642
643        // Add power symbols
644        for pwr in power_symbols {
645            self.coord_map
646                .entry(pwr.location)
647                .or_default()
648                .push(CoordEntry::Power {
649                    name: pwr.net_name.clone(),
650                    is_ground: pwr.is_ground,
651                });
652            self.net_names.insert(pwr.location, pwr.net_name.clone());
653            if pwr.is_ground {
654                self.ground_coords.insert(pwr.location);
655            } else {
656                self.power_coords.insert(pwr.location);
657            }
658        }
659    }
660
661    fn trace_connectivity(&mut self, wires: &[WireView]) {
662        // Initialize union-find: each coordinate is its own parent
663        for &coord in self.coord_map.keys() {
664            self.net_union.insert(coord, coord);
665        }
666
667        // Union wire vertices (each wire connects all its vertices)
668        for wire in wires {
669            if wire.vertices.len() >= 2 {
670                let first = wire.vertices[0];
671                for &vertex in &wire.vertices[1..] {
672                    self.union(first, vertex);
673                }
674            }
675        }
676
677        // Union coordinates that share the same location
678        // (already done implicitly through coord_map)
679    }
680
681    fn find(&mut self, coord: (i32, i32)) -> (i32, i32) {
682        if let std::collections::hash_map::Entry::Vacant(entry) = self.net_union.entry(coord) {
683            entry.insert(coord);
684            return coord;
685        }
686
687        let parent = self.net_union[&coord];
688        if parent == coord {
689            return coord;
690        }
691
692        let root = self.find(parent);
693        self.net_union.insert(coord, root);
694        root
695    }
696
697    fn union(&mut self, a: (i32, i32), b: (i32, i32)) {
698        let root_a = self.find(a);
699        let root_b = self.find(b);
700        if root_a != root_b {
701            self.net_union.insert(root_b, root_a);
702        }
703    }
704
705    fn build_nets(&mut self, _components: &[ComponentView]) -> Vec<NetView> {
706        // Collect coordinates first to avoid borrow issues
707        let coords: Vec<(i32, i32)> = self.coord_map.keys().copied().collect();
708
709        // Group coordinates by their root (same net)
710        let mut net_groups: HashMap<(i32, i32), Vec<(i32, i32)>> = HashMap::new();
711        for coord in coords {
712            let root = self.find(coord);
713            net_groups.entry(root).or_default().push(coord);
714        }
715
716        // Build net views
717        let mut nets = Vec::new();
718        let mut auto_net_id = 0;
719
720        for (_root, coords) in net_groups {
721            // Determine net name from labels or power symbols
722            let mut net_name = None;
723            let mut is_power = false;
724            let mut is_ground = false;
725
726            for &coord in &coords {
727                if let Some(name) = self.net_names.get(&coord) {
728                    net_name = Some(name.clone());
729                }
730                if self.power_coords.contains(&coord) {
731                    is_power = true;
732                }
733                if self.ground_coords.contains(&coord) {
734                    is_ground = true;
735                }
736            }
737
738            // Generate auto name if no label
739            let name = net_name.unwrap_or_else(|| {
740                auto_net_id += 1;
741                format!("Net{}", auto_net_id)
742            });
743
744            // Collect connection points
745            let mut connections = Vec::new();
746            for &coord in &coords {
747                if let Some(entries) = self.coord_map.get(&coord) {
748                    for entry in entries {
749                        match entry {
750                            CoordEntry::PinEnd {
751                                component_designator,
752                                pin_designator,
753                                pin_name,
754                                ..
755                            } => {
756                                connections.push(ConnectionPoint::Pin {
757                                    component_designator: component_designator.clone(),
758                                    pin_designator: pin_designator.clone(),
759                                    pin_name: pin_name.clone(),
760                                });
761                            }
762                            CoordEntry::Port { name, io_type } => {
763                                connections.push(ConnectionPoint::Port {
764                                    name: name.clone(),
765                                    io_type: io_type.clone(),
766                                });
767                            }
768                            CoordEntry::Power { name, is_ground } => {
769                                connections.push(ConnectionPoint::PowerRail {
770                                    net_name: name.clone(),
771                                    is_ground: *is_ground,
772                                });
773                            }
774                            CoordEntry::NetLabel { name } => {
775                                connections.push(ConnectionPoint::NetLabel { name: name.clone() });
776                            }
777                            _ => {} // Skip wire vertices and junctions
778                        }
779                    }
780                }
781            }
782
783            // Only add nets that have actual connections (pins, ports, etc.)
784            let has_connections = connections
785                .iter()
786                .any(|c| matches!(c, ConnectionPoint::Pin { .. }));
787            if has_connections || !connections.is_empty() {
788                nets.push(NetView {
789                    name,
790                    is_power,
791                    is_ground,
792                    connections,
793                });
794            }
795        }
796
797        nets
798    }
799
800    fn update_pin_connections(&self, components: &mut [ComponentView], nets: &[NetView]) {
801        // Build a map from (component, pin) to net name
802        let mut pin_to_net: HashMap<(String, String), String> = HashMap::new();
803        for net in nets {
804            for conn in &net.connections {
805                if let ConnectionPoint::Pin {
806                    component_designator,
807                    pin_designator,
808                    ..
809                } = conn
810                {
811                    pin_to_net.insert(
812                        (component_designator.clone(), pin_designator.clone()),
813                        net.name.clone(),
814                    );
815                }
816            }
817        }
818
819        // Update pins
820        for comp in components {
821            for pin in &mut comp.pins {
822                let key = (comp.designator.clone(), pin.designator.clone());
823                pin.connected_net = pin_to_net.get(&key).cloned();
824            }
825        }
826    }
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832
833    #[test]
834    fn test_electrical_type_display() {
835        assert_eq!(ElectricalType::Input.as_str(), "Input");
836        assert_eq!(ElectricalType::Power.as_str(), "Power");
837    }
838
839    #[test]
840    fn test_connection_point_short_string() {
841        let pin = ConnectionPoint::Pin {
842            component_designator: "U1".to_string(),
843            pin_designator: "1".to_string(),
844            pin_name: "VCC".to_string(),
845        };
846        assert_eq!(pin.to_short_string(), "U1.1 (VCC)");
847
848        let power = ConnectionPoint::PowerRail {
849            net_name: "VCC".to_string(),
850            is_ground: false,
851        };
852        assert_eq!(power.to_short_string(), "PWR:VCC");
853    }
854}