1use crate::io::schdoc::SchDoc;
7use crate::records::sch::*;
8use crate::tree::RecordTree;
9use std::collections::{HashMap, HashSet};
10
11pub use super::common::ElectricalType;
13
14#[derive(Debug, Clone, PartialEq)]
16pub enum ConnectionPoint {
17 Pin {
19 component_designator: String,
20 pin_designator: String,
21 pin_name: String,
22 },
23 Port { name: String, io_type: String },
25 PowerRail { net_name: String, is_ground: bool },
27 NetLabel { name: String },
29}
30
31impl ConnectionPoint {
32 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#[derive(Debug, Clone)]
64pub struct ComponentView {
65 pub designator: String,
67 pub part_name: String,
69 pub description: String,
71 pub value: Option<String>,
73 pub footprint: Option<String>,
75 pub pins: Vec<PinView>,
77 pub parameters: HashMap<String, String>,
79 pub record_index: usize,
81}
82
83#[derive(Debug, Clone)]
85pub struct PinView {
86 pub designator: String,
88 pub name: String,
90 pub electrical_type: ElectricalType,
92 pub connected_net: Option<String>,
94 pub is_hidden: bool,
96 pub hidden_net: Option<String>,
98 pub component_designator: String,
100 pub location: (i32, i32),
102 pub corner: (i32, i32),
104}
105
106#[derive(Debug, Clone)]
108pub struct NetView {
109 pub name: String,
111 pub is_power: bool,
113 pub is_ground: bool,
115 pub connections: Vec<ConnectionPoint>,
117}
118
119#[derive(Debug, Clone)]
121pub struct PortView {
122 pub name: String,
124 pub io_type: String,
126 pub harness: Option<String>,
128 pub connected_net: Option<String>,
130 pub location: (i32, i32),
132 pub record_index: usize,
134}
135
136#[derive(Debug, Clone)]
138pub struct PowerView {
139 pub net_name: String,
141 pub style: String,
143 pub is_ground: bool,
145 pub location: (i32, i32),
147 pub record_index: usize,
149}
150
151#[derive(Debug, Clone)]
153pub struct WireView {
154 pub vertices: Vec<(i32, i32)>,
156 pub record_index: usize,
158}
159
160#[derive(Debug, Clone)]
162pub struct LabelView {
163 pub text: String,
165 pub location: (i32, i32),
167 pub record_index: usize,
169}
170
171#[derive(Debug, Clone)]
173pub struct JunctionView {
174 pub location: (i32, i32),
176 pub record_index: usize,
178}
179
180#[derive(Debug)]
182pub struct SchematicView {
183 pub sheet_name: Option<String>,
185 pub components: Vec<ComponentView>,
187 pub nets: Vec<NetView>,
189 pub ports: Vec<PortView>,
191 pub power_symbols: Vec<PowerView>,
193 pub wires: Vec<WireView>,
195 pub labels: Vec<LabelView>,
197 pub junctions: Vec<JunctionView>,
199 component_index: HashMap<String, usize>,
201 net_index: HashMap<String, usize>,
203}
204
205impl SchematicView {
206 pub fn from_schdoc(doc: &SchDoc) -> Self {
208 let mut builder = SchematicViewBuilder::new(doc);
209 builder.build()
210 }
211
212 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 pub fn get_net(&self, name: &str) -> Option<&NetView> {
221 self.net_index.get(name).map(|&i| &self.nets[i])
222 }
223
224 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 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 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 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
259struct SchematicViewBuilder<'a> {
261 doc: &'a SchDoc,
262 #[allow(dead_code)] tree: RecordTree<SchRecord>,
264 coord_map: HashMap<(i32, i32), Vec<CoordEntry>>,
266 net_union: HashMap<(i32, i32), (i32, i32)>,
268 net_names: HashMap<(i32, i32), String>,
270 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)] electrical_type: ElectricalType,
283 },
284 WireVertex {
285 #[allow(dead_code)] 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 let (components, component_index) = self.extract_components();
319
320 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 self.build_coord_map(
329 &components,
330 &ports,
331 &power_symbols,
332 &wires,
333 &labels,
334 &junctions,
335 );
336
337 self.trace_connectivity(&wires);
339
340 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 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 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 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(), 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 if designator.is_empty() {
429 designator = format!("?{}", components.len());
430 }
431
432 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 for record in &self.doc.primitives {
571 if let SchRecord::SheetHeader(_) = record {
572 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 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 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 for junc in junctions {
615 self.coord_map
616 .entry(junc.location)
617 .or_default()
618 .push(CoordEntry::Junction);
619 }
620
621 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 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 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 for &coord in self.coord_map.keys() {
664 self.net_union.insert(coord, coord);
665 }
666
667 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 }
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 let coords: Vec<(i32, i32)> = self.coord_map.keys().copied().collect();
708
709 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 let mut nets = Vec::new();
718 let mut auto_net_id = 0;
719
720 for (_root, coords) in net_groups {
721 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 let name = net_name.unwrap_or_else(|| {
740 auto_net_id += 1;
741 format!("Net{}", auto_net_id)
742 });
743
744 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 _ => {} }
779 }
780 }
781 }
782
783 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 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 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}