1use std::collections::HashMap;
9use std::fs::File;
10use std::io::BufReader;
11use std::path::{Path, PathBuf};
12
13use serde::Serialize;
14use serde_json;
15
16use crate::ops::categorization::categorize_component;
17use crate::ops::output::*;
18use crate::ops::queries::power::{power_map, separate_power_and_ground};
19use crate::ops::util::{
20 alphanumeric_sort, count_record_types, get_component_designator, get_component_pins,
21 record_type_name, sheet_size_name,
22};
23
24use crate::dump::{fmt_coord, fmt_point};
25use crate::io::SchDoc;
26use crate::records::sch::{
27 PinElectricalType, PortIoType, PowerObjectStyle, SchComponent, SchNetLabel, SchPort,
28 SchPowerObject, SchRecord, SchWire,
29};
30use crate::tree::{RecordId, RecordTree};
31
32fn open_schdoc(path: &Path) -> Result<SchDoc, String> {
34 let file = File::open(path).map_err(|e| format!("Error opening file: {}", e))?;
35 SchDoc::open(BufReader::new(file)).map_err(|e| format!("Error parsing SchDoc: {:?}", e))
36}
37
38fn open_schdoc_boxed(path: &Path) -> Result<SchDoc, Box<dyn std::error::Error>> {
40 let file = File::open(path)?;
41 Ok(SchDoc::open(BufReader::new(file))?)
42}
43
44const BLANK_SCHDOC_TEMPLATE: &[u8] = include_bytes!("../../data/blank/Sheet1.SchDoc");
50
51pub fn cmd_create(path: &Path, template: Option<PathBuf>) -> Result<(), String> {
53 if path.exists() {
54 return Err(format!("File already exists: {}", path.display()));
55 }
56
57 match template {
58 Some(template_path) => {
59 std::fs::copy(&template_path, path)
60 .map_err(|e| format!("Error copying template: {}", e))?;
61 println!("Created SchDoc from template: {}", path.display());
62 println!(" Template: {}", template_path.display());
63 }
64 None => {
65 std::fs::write(path, BLANK_SCHDOC_TEMPLATE)
66 .map_err(|e| format!("Error creating file: {}", e))?;
67 println!("Created empty SchDoc: {}", path.display());
68 }
69 }
70
71 let doc = open_schdoc_boxed(path)
72 .map_err(|e| format!("Error verifying SchDoc: {}", e))?;
73 println!(" Records: {}", doc.primitives.len());
74
75 Ok(())
76}
77
78pub fn cmd_overview(path: &Path) -> Result<SchDocOverview, Box<dyn std::error::Error>> {
84 let doc = open_schdoc(path)?;
85 let tree = RecordTree::from_records(doc.primitives.clone());
86 let counts = count_record_types(&doc);
87
88 let sheet_size = doc
89 .sheet_header()
90 .map(|h| sheet_size_name(h.sheet_size).to_string())
91 .unwrap_or_else(|| "Unknown".to_string());
92
93 let mut categories: HashMap<&'static str, Vec<(String, String, String)>> = HashMap::new();
95 for (id, record) in tree.iter() {
96 if let SchRecord::Component(c) = record {
97 let des = get_component_designator(&tree, id).unwrap_or_else(|| "<none>".to_string());
98 let category = categorize_component(&c.lib_reference, &c.component_description);
99 categories.entry(category).or_default().push((
100 des,
101 c.lib_reference.clone(),
102 c.component_description.clone(),
103 ));
104 }
105 }
106
107 let category_order = [
109 "Microcontroller",
110 "FPGA/CPLD",
111 "Memory",
112 "ADC",
113 "DAC",
114 "Transceiver/PHY",
115 "Clock/Oscillator",
116 "Power Supply",
117 "Amplifier",
118 "Mux/Switch",
119 "Buffer/Driver",
120 "Other IC",
121 "Transistor",
122 "Diode/Protection",
123 "LED",
124 "Capacitor",
125 "Resistor",
126 "Inductor/Ferrite",
127 "Connector",
128 "Test Point",
129 ];
130
131 let mut components_by_category = Vec::new();
132 for &category in &category_order {
133 if let Some(comps) = categories.get(category) {
134 let comp_refs: Vec<SchDocComponentRef> = comps
135 .iter()
136 .map(|(des, lib_ref, desc)| SchDocComponentRef {
137 designator: des.clone(),
138 lib_reference: lib_ref.clone(),
139 description: desc.clone(),
140 })
141 .collect();
142 components_by_category.push((category.to_string(), comp_refs));
143 }
144 }
145
146 let power_nets = power_map(&doc);
148 let (rails, grounds) = separate_power_and_ground(power_nets);
149
150 let power_architecture = PowerArchitecture {
151 power_rails: rails,
152 ground_nets: grounds,
153 };
154
155 let ports: Vec<_> = doc
157 .primitives
158 .iter()
159 .filter_map(|r| {
160 if let SchRecord::Port(p) = r {
161 Some(p)
162 } else {
163 None
164 }
165 })
166 .collect();
167
168 let interfaces = if !ports.is_empty() {
169 let inputs: Vec<String> = ports
170 .iter()
171 .filter(|p| matches!(p.io_type, PortIoType::Input))
172 .map(|p| p.name.clone())
173 .collect();
174 let outputs: Vec<String> = ports
175 .iter()
176 .filter(|p| matches!(p.io_type, PortIoType::Output))
177 .map(|p| p.name.clone())
178 .collect();
179 let bidirectional: Vec<String> = ports
180 .iter()
181 .filter(|p| matches!(p.io_type, PortIoType::Bidirectional))
182 .map(|p| p.name.clone())
183 .collect();
184 let unspecified: Vec<String> = ports
185 .iter()
186 .filter(|p| matches!(p.io_type, PortIoType::Unspecified))
187 .map(|p| p.name.clone())
188 .collect();
189
190 Some(InterfaceSummary {
191 inputs,
192 outputs,
193 bidirectional,
194 unspecified,
195 })
196 } else {
197 None
198 };
199
200 let mut net_labels: HashMap<String, usize> = HashMap::new();
202 for record in &doc.primitives {
203 if let SchRecord::NetLabel(nl) = record {
204 *net_labels.entry(nl.label.text.clone()).or_insert(0) += 1;
205 }
206 }
207
208 let data_buses: Vec<String> = net_labels
209 .iter()
210 .filter(|(n, _)| {
211 n.contains('[') || n.contains("DATA") || n.contains("D0") || n.contains("DQ")
212 })
213 .map(|(n, _)| n.clone())
214 .collect();
215 let address_buses: Vec<String> = net_labels
216 .iter()
217 .filter(|(n, _)| n.contains("ADDR") || n.contains("A0") || n.starts_with("A["))
218 .map(|(n, _)| n.clone())
219 .collect();
220 let control_signals: Vec<String> = net_labels
221 .iter()
222 .filter(|(n, _)| {
223 n.contains("CLK")
224 || n.contains("RESET")
225 || n.contains("EN")
226 || n.contains("CS")
227 || n.contains("WR")
228 || n.contains("RD")
229 || n.contains("_B")
230 })
231 .filter(|(n, _)| !n.contains('['))
232 .map(|(n, _)| n.clone())
233 .collect();
234
235 let key_signals = KeySignals {
236 total_unique_nets: net_labels.len(),
237 data_buses,
238 address_buses,
239 control_signals,
240 };
241
242 let quick_stats = SchDocQuickStats {
244 components: counts.get("Component").copied().unwrap_or(0),
245 wires: counts.get("Wire").copied().unwrap_or(0),
246 junctions: counts.get("Junction").copied().unwrap_or(0),
247 net_labels: counts.get("NetLabel").copied().unwrap_or(0),
248 ports: counts.get("Port").copied().unwrap_or(0),
249 power_symbols: counts.get("PowerObject").copied().unwrap_or(0),
250 };
251
252 Ok(SchDocOverview {
253 path: path.display().to_string(),
254 sheet_size,
255 components_by_category,
256 power_architecture,
257 interfaces,
258 key_signals,
259 quick_stats,
260 })
261}
262
263pub fn cmd_bom(path: &Path) -> Result<SchDocBom, Box<dyn std::error::Error>> {
265 let doc = open_schdoc(path)?;
266 let tree = RecordTree::from_records(doc.primitives.clone());
267
268 let mut bom: HashMap<String, Vec<(String, String)>> = HashMap::new();
270 for (id, record) in tree.iter() {
271 if let SchRecord::Component(c) = record {
272 let des = get_component_designator(&tree, id).unwrap_or_else(|| "<none>".to_string());
273 bom.entry(c.lib_reference.clone())
274 .or_default()
275 .push((des, c.component_description.clone()));
276 }
277 }
278
279 let mut sorted: Vec<_> = bom.iter().collect();
281 sorted.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
282
283 let total_components = sorted.iter().map(|(_, items)| items.len()).sum();
284 let unique_parts = bom.len();
285
286 let items: Vec<BomItem> = sorted
287 .iter()
288 .map(|(lib_ref, comps)| {
289 let mut designators: Vec<_> = comps.iter().map(|(d, _)| d.clone()).collect();
290 designators.sort_by(|a, b| alphanumeric_sort(a, b));
291
292 let description = comps
293 .first()
294 .map(|(_, desc)| desc.clone())
295 .unwrap_or_default();
296
297 BomItem {
298 lib_reference: lib_ref.to_string(),
299 quantity: comps.len(),
300 designators,
301 description,
302 }
303 })
304 .collect();
305
306 Ok(SchDocBom {
307 path: path.display().to_string(),
308 total_components,
309 unique_parts,
310 items,
311 })
312}
313
314#[allow(clippy::type_complexity)]
316pub fn cmd_netlist(
317 path: &Path,
318 net_filter: Option<String>,
319 min_connections: usize,
320) -> Result<SchDocNetlist, Box<dyn std::error::Error>> {
321 let doc = open_schdoc(path)?;
322 let tree = RecordTree::from_records(doc.primitives.clone());
323
324 let mut pin_locations: HashMap<(i32, i32), Vec<(String, String, String)>> = HashMap::new();
326 for (id, record) in tree.iter() {
327 if let SchRecord::Component(_) = record {
328 let des =
329 get_component_designator(&tree, id).unwrap_or_else(|| format!("?{}", id.index()));
330 for (pin_des, pin_name, corner_x, corner_y) in get_component_pins(&tree, id) {
331 pin_locations
332 .entry((corner_x, corner_y))
333 .or_default()
334 .push((des.clone(), pin_des, pin_name));
335 }
336 }
337 }
338
339 let mut net_at_location: HashMap<(i32, i32), String> = HashMap::new();
341 for record in &doc.primitives {
342 match record {
343 SchRecord::NetLabel(nl) => {
344 net_at_location.insert(
345 (nl.label.graphical.location_x, nl.label.graphical.location_y),
346 nl.label.text.clone(),
347 );
348 }
349 SchRecord::PowerObject(p) => {
350 net_at_location.insert(
351 (p.graphical.location_x, p.graphical.location_y),
352 p.text.clone(),
353 );
354 }
355 _ => {}
356 }
357 }
358
359 let mut nets: HashMap<String, Vec<String>> = HashMap::new();
361 let proximity_threshold = 100000; for ((net_x, net_y), net_name) in &net_at_location {
364 for ((pin_x, pin_y), pins) in &pin_locations {
365 if (net_x - pin_x).abs() < proximity_threshold
366 && (net_y - pin_y).abs() < proximity_threshold
367 {
368 for (comp_des, pin_des, pin_name) in pins {
369 nets.entry(net_name.clone())
370 .or_default()
371 .push(format!("{}.{} ({})", comp_des, pin_des, pin_name));
372 }
373 }
374 }
375 }
376
377 let mut filtered_nets: Vec<_> = nets
379 .iter()
380 .filter(|(name, conns)| {
381 let pass_filter = match &net_filter {
382 Some(f) if f.contains('*') => name.contains(&f.replace('*', "")),
383 Some(f) => name.eq_ignore_ascii_case(f),
384 None => true,
385 };
386 pass_filter && conns.len() >= min_connections
387 })
388 .collect();
389 filtered_nets.sort_by(|a, b| a.0.cmp(b.0));
390
391 let net_connections: Vec<NetConnection> = filtered_nets
392 .iter()
393 .map(|(name, conns)| NetConnection {
394 net_name: (*name).clone(),
395 connections: conns.to_vec(),
396 })
397 .collect();
398
399 Ok(SchDocNetlist {
400 path: path.display().to_string(),
401 filter: net_filter,
402 min_connections,
403 total_nets: net_connections.len(),
404 nets: net_connections,
405 })
406}
407
408pub fn cmd_power_map(path: &Path) -> Result<SchDocPowerMap, Box<dyn std::error::Error>> {
410 let doc = open_schdoc_boxed(path)?;
411 let tree = RecordTree::from_records(doc.primitives.clone());
412
413 let mut comp_info: HashMap<RecordId, (String, String)> = HashMap::new();
415 let mut power_pins: HashMap<RecordId, Vec<(String, String)>> = HashMap::new(); for (id, record) in tree.iter() {
418 if let SchRecord::Component(c) = record {
419 let des =
420 get_component_designator(&tree, id).unwrap_or_else(|| format!("?{}", id.index()));
421 comp_info.insert(id, (des.clone(), c.lib_reference.clone()));
422
423 for (_, child) in tree.children(id) {
425 if let SchRecord::Pin(p) = child {
426 let name_upper = p.name.to_uppercase();
427 if name_upper.contains("VCC")
428 || name_upper.contains("VDD")
429 || name_upper.contains("GND")
430 || name_upper.contains("VSS")
431 || name_upper.contains("AVCC")
432 || name_upper.contains("AVDD")
433 || name_upper.contains("AGND")
434 || name_upper.contains("DVCC")
435 || name_upper.contains("DVDD")
436 || name_upper.contains("DGND")
437 || name_upper.contains("VIN")
438 || name_upper.contains("VOUT")
439 || name_upper.contains("PWR")
440 || name_upper.contains("POWER")
441 || format!("{:?}", p.electrical).contains("Power")
442 {
443 power_pins
444 .entry(id)
445 .or_default()
446 .push((p.designator.clone(), p.name.clone()));
447 }
448 }
449 }
450 }
451 }
452
453 let mut power_nets: HashMap<String, Vec<(i32, i32)>> = HashMap::new();
455 for record in &doc.primitives {
456 if let SchRecord::PowerObject(p) = record {
457 power_nets
458 .entry(p.text.clone())
459 .or_default()
460 .push((p.graphical.location_x, p.graphical.location_y));
461 }
462 }
463
464 let mut rails: Vec<_> = power_nets
466 .iter()
467 .filter(|(name, _)| {
468 !name.to_uppercase().contains("GND") && !name.to_uppercase().contains("VSS")
469 })
470 .collect();
471 let mut grounds: Vec<_> = power_nets
472 .iter()
473 .filter(|(name, _)| {
474 name.to_uppercase().contains("GND") || name.to_uppercase().contains("VSS")
475 })
476 .collect();
477
478 rails.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
479 grounds.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
480
481 let power_rails: Vec<PowerRail> = rails
483 .iter()
484 .map(|(net_name, locations)| {
485 let mut consumers = Vec::new();
487 for (comp_id, pins) in &power_pins {
488 if let Some((des, _lib_ref)) = comp_info.get(comp_id) {
489 for (_pin_des, pin_name) in pins {
490 if pin_name.to_uppercase().contains(&net_name.to_uppercase())
491 || (net_name.contains("3V3") && pin_name.contains("3V3"))
492 || (net_name.contains("5V") && pin_name.contains("5V"))
493 || (net_name.contains("1V")
494 && (pin_name.contains("1V") || pin_name.contains("VDD")))
495 {
496 consumers.push(format!("{} ({})", des, pin_name));
497 }
498 }
499 }
500 }
501 consumers.sort();
502 consumers.dedup();
503
504 PowerRail {
505 net_name: (*net_name).clone(),
506 symbol_count: locations.len(),
507 consumers,
508 }
509 })
510 .collect();
511
512 let ground_nets: Vec<GroundNet> = grounds
514 .iter()
515 .map(|(net_name, locations)| GroundNet {
516 net_name: (*net_name).clone(),
517 symbol_count: locations.len(),
518 })
519 .collect();
520
521 let mut powered_components: Vec<_> = power_pins
523 .iter()
524 .filter_map(|(id, pins)| {
525 comp_info.get(id).map(|(des, lib_ref)| PoweredComponent {
526 designator: des.clone(),
527 lib_reference: lib_ref.clone(),
528 power_pin_count: pins.len(),
529 })
530 })
531 .collect();
532 powered_components.sort_by(|a, b| b.power_pin_count.cmp(&a.power_pin_count));
533
534 Ok(SchDocPowerMap {
535 path: path.display().to_string(),
536 power_rails,
537 ground_nets,
538 powered_components,
539 })
540}
541
542pub fn cmd_blocks(path: &Path, show_all: bool) -> Result<SchDocBlocks, Box<dyn std::error::Error>> {
544 let doc = open_schdoc_boxed(path)?;
545 let tree = RecordTree::from_records(doc.primitives.clone());
546
547 let mut blocks: Vec<BlockInfo> = Vec::new();
548
549 for (id, record) in tree.iter() {
550 if let SchRecord::Component(c) = record {
551 let des = get_component_designator(&tree, id).unwrap_or_else(|| "<none>".to_string());
552 let category = categorize_component(&c.lib_reference, &c.component_description);
553
554 if !show_all {
556 let skip_categories = ["Capacitor", "Resistor", "Inductor/Ferrite", "Test Point"];
557 if skip_categories.contains(&category) {
558 continue;
559 }
560 }
561
562 let mut power_pins = Vec::new();
563 let mut input_pins = Vec::new();
564 let mut output_pins = Vec::new();
565 let mut bidir_pins = Vec::new();
566
567 for (_, child) in tree.children(id) {
569 if let SchRecord::Pin(p) = child {
570 if p.is_hidden() {
571 continue;
572 }
573 let pin_info = if p.name.is_empty() {
574 p.designator.clone()
575 } else if p.name.len() > 15 {
576 format!("{}...", &p.name[..12])
577 } else {
578 p.name.clone()
579 };
580
581 match p.electrical {
582 PinElectricalType::Power => power_pins.push(pin_info),
583 PinElectricalType::Input => input_pins.push(pin_info),
584 PinElectricalType::Output => output_pins.push(pin_info),
585 PinElectricalType::InputOutput => bidir_pins.push(pin_info),
586 _ => bidir_pins.push(pin_info), }
588 }
589 }
590
591 blocks.push(BlockInfo {
592 designator: des,
593 lib_reference: c.lib_reference.clone(),
594 description: c.component_description.clone(),
595 category: category.to_string(),
596 power_pins,
597 input_pins,
598 output_pins,
599 bidir_pins,
600 });
601 }
602 }
603
604 let category_priority: HashMap<&str, usize> = [
606 ("Microcontroller", 0),
607 ("FPGA/CPLD", 1),
608 ("Memory", 2),
609 ("ADC", 3),
610 ("DAC", 4),
611 ("Transceiver/PHY", 5),
612 ("Clock/Oscillator", 6),
613 ("Power Supply", 7),
614 ("Amplifier", 8),
615 ("Mux/Switch", 9),
616 ("Buffer/Driver", 10),
617 ("Other IC", 11),
618 ]
619 .iter()
620 .cloned()
621 .collect();
622
623 blocks.sort_by(|a, b| {
624 let pa = category_priority.get(a.category.as_str()).unwrap_or(&99);
625 let pb = category_priority.get(b.category.as_str()).unwrap_or(&99);
626 pa.cmp(pb)
627 .then_with(|| alphanumeric_sort(&a.designator, &b.designator))
628 });
629
630 Ok(SchDocBlocks {
631 path: path.display().to_string(),
632 blocks,
633 show_all,
634 })
635}
636
637pub fn cmd_project(paths: &[PathBuf]) -> Result<SchDocProjectAnalysis, Box<dyn std::error::Error>> {
639 if paths.is_empty() {
640 return Err("No schematic files specified".into());
641 }
642
643 struct LocalSheetInfo {
645 name: String,
646 components: usize,
647 ports: Vec<(String, String)>, power_nets: Vec<String>,
649 unique_nets: Vec<String>,
650 }
651
652 let mut sheets: Vec<LocalSheetInfo> = Vec::new();
653 let mut all_ports: HashMap<String, Vec<(String, String)>> = HashMap::new(); for path in paths {
656 let doc = match open_schdoc(path) {
657 Ok(d) => d,
658 Err(_e) => {
659 continue;
661 }
662 };
663
664 let sheet_name = path
665 .file_stem()
666 .and_then(|s| s.to_str())
667 .unwrap_or("unknown")
668 .to_string();
669
670 let component_count = doc
671 .primitives
672 .iter()
673 .filter(|r| matches!(r, SchRecord::Component(_)))
674 .count();
675
676 let mut ports: Vec<(String, String)> = Vec::new();
677 for record in &doc.primitives {
678 if let SchRecord::Port(p) = record {
679 let io = match p.io_type {
680 PortIoType::Input => "IN",
681 PortIoType::Output => "OUT",
682 PortIoType::Bidirectional => "BIDIR",
683 PortIoType::Unspecified => "BUS",
684 };
685 ports.push((p.name.clone(), io.to_string()));
686 all_ports
687 .entry(p.name.clone())
688 .or_default()
689 .push((sheet_name.clone(), io.to_string()));
690 }
691 }
692
693 let mut power_nets: Vec<String> = doc
694 .primitives
695 .iter()
696 .filter_map(|r| {
697 if let SchRecord::PowerObject(p) = r {
698 Some(p.text.clone())
699 } else {
700 None
701 }
702 })
703 .collect();
704 power_nets.sort();
705 power_nets.dedup();
706
707 let mut unique_nets: Vec<String> = doc
708 .primitives
709 .iter()
710 .filter_map(|r| {
711 if let SchRecord::NetLabel(nl) = r {
712 Some(nl.label.text.clone())
713 } else {
714 None
715 }
716 })
717 .collect();
718 unique_nets.sort();
719 unique_nets.dedup();
720
721 sheets.push(LocalSheetInfo {
722 name: sheet_name,
723 components: component_count,
724 ports,
725 power_nets,
726 unique_nets,
727 });
728 }
729
730 let output_sheets: Vec<SheetInfo> = sheets
732 .iter()
733 .map(|s| SheetInfo {
734 name: s.name.clone(),
735 component_count: s.components,
736 port_count: s.ports.len(),
737 net_count: s.unique_nets.len(),
738 ports: s.ports.clone(),
739 power_nets: s.power_nets.clone(),
740 })
741 .collect();
742
743 let mut connections: Vec<_> = all_ports
745 .iter()
746 .filter(|(_, sheets)| sheets.len() > 1)
747 .collect();
748
749 connections.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
750
751 let inter_sheet_connections: Vec<InterSheetConnection> = connections
752 .into_iter()
753 .map(|(port_name, connected_sheets)| InterSheetConnection {
754 port_name: port_name.to_string(),
755 connected_sheets: connected_sheets.clone(),
756 })
757 .collect();
758
759 Ok(SchDocProjectAnalysis {
760 sheet_count: output_sheets.len(),
761 sheets: output_sheets,
762 inter_sheet_connections,
763 })
764}
765
766pub fn cmd_signal_flow(
768 path: &Path,
769 signal: &str,
770) -> Result<SchDocSignalFlow, Box<dyn std::error::Error>> {
771 let doc = open_schdoc(path)?;
772 let tree = RecordTree::from_records(doc.primitives.clone());
773
774 let matching_nets: Vec<_> = doc
776 .primitives
777 .iter()
778 .filter_map(|r| {
779 if let SchRecord::NetLabel(nl) = r {
780 if nl.label.text.eq_ignore_ascii_case(signal)
781 || nl
782 .label
783 .text
784 .to_uppercase()
785 .contains(&signal.to_uppercase())
786 {
787 Some((
788 nl.label.text.clone(),
789 nl.label.graphical.location_x,
790 nl.label.graphical.location_y,
791 ))
792 } else {
793 None
794 }
795 } else {
796 None
797 }
798 })
799 .collect();
800
801 let matching_power: Vec<_> = doc
803 .primitives
804 .iter()
805 .filter_map(|r| {
806 if let SchRecord::PowerObject(p) = r {
807 if p.text.eq_ignore_ascii_case(signal)
808 || p.text.to_uppercase().contains(&signal.to_uppercase())
809 {
810 Some((
811 p.text.clone(),
812 p.graphical.location_x,
813 p.graphical.location_y,
814 ))
815 } else {
816 None
817 }
818 } else {
819 None
820 }
821 })
822 .collect();
823
824 let matching_ports: Vec<_> = doc
826 .primitives
827 .iter()
828 .filter_map(|r| {
829 if let SchRecord::Port(p) = r {
830 if p.name.eq_ignore_ascii_case(signal)
831 || p.name.to_uppercase().contains(&signal.to_uppercase())
832 {
833 Some((p.name.clone(), format!("{:?}", p.io_type)))
834 } else {
835 None
836 }
837 } else {
838 None
839 }
840 })
841 .collect();
842
843 if matching_nets.is_empty() && matching_power.is_empty() && matching_ports.is_empty() {
844 return Ok(SchDocSignalFlow {
845 path: path.display().to_string(),
846 signal: signal.to_string(),
847 trace_found: false,
848 trace: None,
849 });
850 }
851
852 let mut trace_path = Vec::new();
854
855 for (name, x, y) in &matching_nets {
857 trace_path.push(format!("NetLabel {} at {}", name, fmt_point(*x, *y)));
858 }
859
860 for (name, x, y) in &matching_power {
862 trace_path.push(format!("Power {} at {}", name, fmt_point(*x, *y)));
863 }
864
865 for (name, io_type) in &matching_ports {
867 trace_path.push(format!("Port {} [{}]", name, io_type));
868 }
869
870 let signal_upper = signal.to_uppercase();
872 let mut destinations = Vec::new();
873
874 for (id, record) in tree.iter() {
875 if let SchRecord::Component(_c) = record {
876 let des = get_component_designator(&tree, id).unwrap_or_default();
877
878 for (_, child) in tree.children(id) {
879 if let SchRecord::Pin(p) = child {
880 if p.name.to_uppercase().contains(&signal_upper)
881 || p.designator.to_uppercase().contains(&signal_upper)
882 {
883 destinations.push(format!(
884 "{}.{} ({}) - {:?}",
885 des, p.designator, p.name, p.electrical
886 ));
887 }
888 }
889 }
890 }
891 }
892
893 let source = if !matching_ports.is_empty() {
894 format!("Port {}", matching_ports[0].0)
895 } else if !matching_power.is_empty() {
896 format!("Power {}", matching_power[0].0)
897 } else {
898 format!("Net {}", matching_nets[0].0)
899 };
900
901 Ok(SchDocSignalFlow {
902 path: path.display().to_string(),
903 signal: signal.to_string(),
904 trace_found: true,
905 trace: Some(SignalTrace {
906 source,
907 path: trace_path,
908 destinations,
909 }),
910 })
911}
912
913pub fn cmd_info(path: &Path) -> Result<SchDocInfo, Box<dyn std::error::Error>> {
919 let doc = open_schdoc(path)?;
920 let counts = count_record_types(&doc);
921
922 let sheet_info = doc.sheet_header().map(|header| {
924 let custom_dimensions = if header.custom_x > 0 || header.custom_y > 0 {
925 Some((
926 fmt_coord(header.custom_x * 10000),
927 fmt_coord(header.custom_y * 10000),
928 ))
929 } else {
930 None
931 };
932 SheetInfoDetails {
933 size: sheet_size_name(header.sheet_size).to_string(),
934 size_style: header.sheet_size,
935 custom_dimensions,
936 fonts_defined: header.font_id_count,
937 }
938 });
939
940 let primitive_summary = PrimitiveSummary {
942 total_primitives: doc.primitives.len(),
943 components: counts.get("Component").copied().unwrap_or(0),
944 wires: counts.get("Wire").copied().unwrap_or(0),
945 net_labels: counts.get("NetLabel").copied().unwrap_or(0),
946 ports: counts.get("Port").copied().unwrap_or(0),
947 power_objects: counts.get("PowerObject").copied().unwrap_or(0),
948 junctions: counts.get("Junction").copied().unwrap_or(0),
949 pins: counts.get("Pin").copied().unwrap_or(0),
950 };
951
952 let mut net_names: Vec<String> = doc
954 .primitives
955 .iter()
956 .filter_map(|r| {
957 if let SchRecord::NetLabel(nl) = r {
958 Some(nl.label.text.clone())
959 } else {
960 None
961 }
962 })
963 .collect();
964 net_names.sort();
965 net_names.dedup();
966
967 let mut power_nets: Vec<String> = doc
969 .primitives
970 .iter()
971 .filter_map(|r| {
972 if let SchRecord::PowerObject(p) = r {
973 Some(p.text.clone())
974 } else {
975 None
976 }
977 })
978 .collect();
979 power_nets.sort();
980 power_nets.dedup();
981
982 Ok(SchDocInfo {
983 path: path.display().to_string(),
984 sheet_info,
985 primitive_summary,
986 unique_nets: net_names,
987 power_nets,
988 })
989}
990
991pub fn cmd_stats(path: &Path) -> Result<SchDocStats, Box<dyn std::error::Error>> {
993 let doc = open_schdoc(path)?;
994 let counts = count_record_types(&doc);
995
996 let mut record_types: Vec<(String, usize)> = counts
997 .into_iter()
998 .map(|(name, count)| (name.to_string(), count))
999 .collect();
1000 record_types.sort_by(|a, b| b.1.cmp(&a.1));
1001
1002 Ok(SchDocStats {
1003 path: path.display().to_string(),
1004 total_primitives: doc.primitives.len(),
1005 record_types,
1006 })
1007}
1008
1009pub fn cmd_components(
1011 path: &Path,
1012 verbose: bool,
1013) -> Result<SchDocComponentList, Box<dyn std::error::Error>> {
1014 let doc = open_schdoc(path)?;
1015 let tree = RecordTree::from_records(doc.primitives.clone());
1016
1017 let mut component_data: Vec<(RecordId, &SchComponent, Option<String>)> = Vec::new();
1018 for (id, record) in tree.iter() {
1019 if let SchRecord::Component(c) = record {
1020 let designator = get_component_designator(&tree, id);
1021 component_data.push((id, c, designator));
1022 }
1023 }
1024
1025 component_data.sort_by(|a, b| {
1027 let a_des = a.2.as_deref().unwrap_or("");
1028 let b_des = b.2.as_deref().unwrap_or("");
1029 alphanumeric_sort(a_des, b_des)
1030 });
1031
1032 let components = component_data
1033 .iter()
1034 .map(|(id, comp, designator)| {
1035 let child_count = if verbose {
1036 Some(tree.child_count(*id))
1037 } else {
1038 None
1039 };
1040 SchDocComponentInfo {
1041 designator: designator.clone().unwrap_or_else(|| "<none>".to_string()),
1042 lib_reference: comp.lib_reference.clone(),
1043 description: comp.component_description.clone(),
1044 location: fmt_point(comp.graphical.location_x, comp.graphical.location_y),
1045 parts: comp.part_count,
1046 child_count,
1047 }
1048 })
1049 .collect();
1050
1051 Ok(SchDocComponentList {
1052 path: path.display().to_string(),
1053 total_components: component_data.len(),
1054 components,
1055 })
1056}
1057
1058pub fn cmd_component(
1060 path: &Path,
1061 designator: &str,
1062 show_children: bool,
1063) -> Result<SchDocComponentDetail, Box<dyn std::error::Error>> {
1064 let doc = open_schdoc(path)?;
1065 let tree = RecordTree::from_records(doc.primitives.clone());
1066
1067 let component_id = if let Ok(index) = designator.parse::<usize>() {
1069 let mut comp_idx = 0;
1071 let mut found_id = None;
1072 for (id, record) in tree.iter() {
1073 if matches!(record, SchRecord::Component(_)) {
1074 if comp_idx == index {
1075 found_id = Some(id);
1076 break;
1077 }
1078 comp_idx += 1;
1079 }
1080 }
1081 found_id.ok_or_else(|| format!("Component index {} not found", index))?
1082 } else {
1083 let mut found_id = None;
1085 for (id, record) in tree.iter() {
1086 if matches!(record, SchRecord::Component(_)) {
1087 if let Some(des) = get_component_designator(&tree, id) {
1088 if des.eq_ignore_ascii_case(designator) {
1089 found_id = Some(id);
1090 break;
1091 }
1092 }
1093 }
1094 }
1095 found_id.ok_or_else(|| format!("Component '{}' not found", designator))?
1096 };
1097
1098 let comp = match tree.get(component_id) {
1099 Some(SchRecord::Component(c)) => c,
1100 _ => return Err("Invalid component".into()),
1101 };
1102
1103 let actual_designator = get_component_designator(&tree, component_id);
1104
1105 let children: Vec<_> = tree.children(component_id).collect();
1107 let mut pin_infos = Vec::new();
1108 let mut param_infos = Vec::new();
1109 let mut designator_infos = Vec::new();
1110 let mut graphics_count = 0;
1111
1112 for (_id, child) in &children {
1113 match child {
1114 SchRecord::Pin(p) => {
1115 pin_infos.push(SchDocPinInfo {
1116 designator: p.designator.clone(),
1117 name: p.name.clone(),
1118 electrical_type: format!("{:?}", p.electrical),
1119 hidden: p.is_hidden(),
1120 });
1121 }
1122 SchRecord::Parameter(p) => {
1123 param_infos.push(SchDocParameter {
1124 name: p.name.clone(),
1125 value: p.label.text.clone(),
1126 });
1127 }
1128 SchRecord::Designator(d) => {
1129 designator_infos.push(SchDocDesignator {
1130 name: d.param.name.clone(),
1131 value: d.param.label.text.clone(),
1132 });
1133 }
1134 _ => graphics_count += 1,
1135 }
1136 }
1137
1138 Ok(SchDocComponentDetail {
1139 designator: actual_designator.unwrap_or_else(|| "<none>".to_string()),
1140 lib_reference: comp.lib_reference.clone(),
1141 description: comp.component_description.clone(),
1142 location: fmt_point(comp.graphical.location_x, comp.graphical.location_y),
1143 parts: comp.part_count,
1144 display_modes: comp.display_mode_count,
1145 current_part: comp.current_part_id,
1146 unique_id: comp.unique_id.clone(),
1147 child_primitive_count: children.len(),
1148 pins: pin_infos,
1149 parameters: param_infos,
1150 designators: designator_infos,
1151 graphic_primitive_count: if show_children {
1152 Some(graphics_count)
1153 } else {
1154 None
1155 },
1156 })
1157}
1158
1159pub fn cmd_wires(
1161 path: &Path,
1162 limit: Option<usize>,
1163) -> Result<SchDocWireList, Box<dyn std::error::Error>> {
1164 let doc = open_schdoc(path)?;
1165
1166 let wires: Vec<&SchWire> = doc
1167 .primitives
1168 .iter()
1169 .filter_map(|r| {
1170 if let SchRecord::Wire(w) = r {
1171 Some(w)
1172 } else {
1173 None
1174 }
1175 })
1176 .collect();
1177
1178 let display_count = limit.unwrap_or(wires.len()).min(wires.len());
1179
1180 let wire_infos: Vec<WireInfo> = wires
1181 .iter()
1182 .take(display_count)
1183 .enumerate()
1184 .map(|(i, wire)| {
1185 let vertices = &wire.vertices;
1186 let (start, end_or_segments) = if vertices.len() == 2 {
1187 (
1188 fmt_point(vertices[0].0, vertices[0].1),
1189 fmt_point(vertices[1].0, vertices[1].1),
1190 )
1191 } else {
1192 let start = if vertices.is_empty() {
1193 "(empty)".to_string()
1194 } else {
1195 fmt_point(vertices[0].0, vertices[0].1)
1196 };
1197 let segments = format!("{} segments", vertices.len().saturating_sub(1));
1198 (start, segments)
1199 };
1200 WireInfo {
1201 index: i,
1202 start,
1203 end_or_segments,
1204 }
1205 })
1206 .collect();
1207
1208 Ok(SchDocWireList {
1209 path: path.display().to_string(),
1210 total_wires: wires.len(),
1211 wires: wire_infos,
1212 })
1213}
1214
1215pub fn cmd_nets(
1217 path: &Path,
1218 group: bool,
1219) -> Result<SchDocNetLabelList, Box<dyn std::error::Error>> {
1220 let doc = open_schdoc(path)?;
1221
1222 let net_labels: Vec<&SchNetLabel> = doc
1223 .primitives
1224 .iter()
1225 .filter_map(|r| {
1226 if let SchRecord::NetLabel(nl) = r {
1227 Some(nl)
1228 } else {
1229 None
1230 }
1231 })
1232 .collect();
1233
1234 let (grouped, individual) = if group {
1235 let mut grouped_map: HashMap<&str, Vec<&SchNetLabel>> = HashMap::new();
1236 for nl in &net_labels {
1237 grouped_map.entry(&nl.label.text).or_default().push(nl);
1238 }
1239
1240 let mut sorted: Vec<_> = grouped_map.iter().collect();
1241 sorted.sort_by(|a, b| a.0.cmp(b.0));
1242
1243 let grouped_result: Vec<(String, usize)> = sorted
1244 .into_iter()
1245 .map(|(name, labels)| (name.to_string(), labels.len()))
1246 .collect();
1247
1248 (Some(grouped_result), None)
1249 } else {
1250 let individual_result: Vec<NetLabelInfo> = net_labels
1251 .iter()
1252 .map(|nl| NetLabelInfo {
1253 net_name: nl.label.text.clone(),
1254 location: fmt_point(nl.label.graphical.location_x, nl.label.graphical.location_y),
1255 })
1256 .collect();
1257
1258 (None, Some(individual_result))
1259 };
1260
1261 Ok(SchDocNetLabelList {
1262 path: path.display().to_string(),
1263 total_net_labels: net_labels.len(),
1264 group_by_name: group,
1265 grouped,
1266 individual,
1267 })
1268}
1269
1270pub fn cmd_ports(path: &Path) -> Result<SchDocPortList, Box<dyn std::error::Error>> {
1272 let doc = open_schdoc(path)?;
1273
1274 let ports: Vec<&SchPort> = doc
1275 .primitives
1276 .iter()
1277 .filter_map(|r| {
1278 if let SchRecord::Port(p) = r {
1279 Some(p)
1280 } else {
1281 None
1282 }
1283 })
1284 .collect();
1285
1286 let port_infos: Vec<PortInfo> = ports
1287 .iter()
1288 .map(|port| {
1289 let io_type = match port.io_type {
1290 PortIoType::Unspecified => "Unspec",
1291 PortIoType::Output => "Output",
1292 PortIoType::Input => "Input",
1293 PortIoType::Bidirectional => "Bidir",
1294 };
1295 PortInfo {
1296 name: port.name.clone(),
1297 io_type: io_type.to_string(),
1298 location: fmt_point(port.graphical.location_x, port.graphical.location_y),
1299 }
1300 })
1301 .collect();
1302
1303 Ok(SchDocPortList {
1304 path: path.display().to_string(),
1305 total_ports: ports.len(),
1306 ports: port_infos,
1307 })
1308}
1309
1310pub fn cmd_power(path: &Path, group: bool) -> Result<SchDocPowerList, Box<dyn std::error::Error>> {
1312 let doc = open_schdoc(path)?;
1313
1314 let power_objects: Vec<&SchPowerObject> = doc
1315 .primitives
1316 .iter()
1317 .filter_map(|r| {
1318 if let SchRecord::PowerObject(p) = r {
1319 Some(p)
1320 } else {
1321 None
1322 }
1323 })
1324 .collect();
1325
1326 let (grouped, individual) = if group {
1327 let mut grouped_map: HashMap<&str, Vec<&SchPowerObject>> = HashMap::new();
1328 for p in &power_objects {
1329 grouped_map.entry(&p.text).or_default().push(p);
1330 }
1331
1332 let mut sorted: Vec<_> = grouped_map.iter().collect();
1333 sorted.sort_by(|a, b| a.0.cmp(b.0));
1334
1335 let grouped_result: Vec<(String, usize)> = sorted
1336 .into_iter()
1337 .map(|(name, objs)| (name.to_string(), objs.len()))
1338 .collect();
1339
1340 (Some(grouped_result), None)
1341 } else {
1342 let individual_result: Vec<PowerObjectInfo> = power_objects
1343 .iter()
1344 .map(|p| {
1345 let style = match p.style {
1346 PowerObjectStyle::Arrow => "Arrow",
1347 PowerObjectStyle::Bar => "Bar",
1348 PowerObjectStyle::Wave => "Wave",
1349 PowerObjectStyle::Ground => "Ground",
1350 PowerObjectStyle::PowerGround => "PowerGnd",
1351 PowerObjectStyle::SignalGround => "SignalGnd",
1352 PowerObjectStyle::EarthGround => "EarthGnd",
1353 PowerObjectStyle::Circle => "Circle",
1354 };
1355 PowerObjectInfo {
1356 net: p.text.clone(),
1357 style: style.to_string(),
1358 location: fmt_point(p.graphical.location_x, p.graphical.location_y),
1359 }
1360 })
1361 .collect();
1362
1363 (None, Some(individual_result))
1364 };
1365
1366 Ok(SchDocPowerList {
1367 path: path.display().to_string(),
1368 total_power_objects: power_objects.len(),
1369 group_by_net: group,
1370 grouped,
1371 individual,
1372 })
1373}
1374
1375pub fn cmd_pins(
1377 path: &Path,
1378 component_filter: Option<String>,
1379 _unconnected: bool,
1380) -> Result<SchDocPinList, Box<dyn std::error::Error>> {
1381 let doc = open_schdoc(path)?;
1382 let tree = RecordTree::from_records(doc.primitives.clone());
1383
1384 let mut pin_details = Vec::new();
1385 let total_pins: usize;
1386
1387 if let Some(ref comp_des) = component_filter {
1389 let mut found_component = None;
1391 for (id, record) in tree.iter() {
1392 if matches!(record, SchRecord::Component(_)) {
1393 if let Some(des) = get_component_designator(&tree, id) {
1394 if des.eq_ignore_ascii_case(comp_des) {
1395 found_component = Some(id);
1396 break;
1397 }
1398 }
1399 }
1400 }
1401
1402 if let Some(comp_id) = found_component {
1403 let des = get_component_designator(&tree, comp_id).unwrap_or_default();
1404
1405 let pins: Vec<_> = tree
1406 .children(comp_id)
1407 .filter_map(|(_, r)| {
1408 if let SchRecord::Pin(p) = r {
1409 Some(p)
1410 } else {
1411 None
1412 }
1413 })
1414 .collect();
1415
1416 total_pins = pins.len();
1417
1418 for pin in pins {
1419 pin_details.push(SchDocPinDetail {
1420 component: des.clone(),
1421 designator: pin.designator.clone(),
1422 name: pin.name.clone(),
1423 electrical_type: format!("{:?}", pin.electrical),
1424 location: fmt_point(0, 0), });
1426 }
1427 } else {
1428 return Err(format!("Component '{}' not found", comp_des).into());
1429 }
1430 } else {
1431 let mut comp_map: HashMap<RecordId, String> = HashMap::new();
1433 for (id, record) in tree.iter() {
1434 if let SchRecord::Component(_) = record {
1435 if let Some(des) = get_component_designator(&tree, id) {
1436 comp_map.insert(id, des);
1437 }
1438 }
1439 }
1440
1441 for (id, record) in tree.iter() {
1443 if let SchRecord::Pin(p) = record {
1444 if let Some(parent_id) = tree.parent_id(id) {
1446 let comp_des = comp_map.get(&parent_id).cloned().unwrap_or_default();
1447 pin_details.push(SchDocPinDetail {
1448 component: comp_des,
1449 designator: p.designator.clone(),
1450 name: p.name.clone(),
1451 electrical_type: format!("{:?}", p.electrical),
1452 location: fmt_point(0, 0),
1453 });
1454 }
1455 }
1456 }
1457
1458 total_pins = pin_details.len();
1459 }
1460
1461 Ok(SchDocPinList {
1462 path: path.display().to_string(),
1463 total_pins,
1464 filter: component_filter,
1465 pins: pin_details,
1466 })
1467}
1468
1469pub fn cmd_hierarchy(
1471 path: &Path,
1472 max_depth: Option<usize>,
1473 from_designator: Option<String>,
1474) -> Result<SchDocHierarchy, Box<dyn std::error::Error>> {
1475 let doc = open_schdoc(path)?;
1476 let tree = RecordTree::from_records(doc.primitives.clone());
1477
1478 let start_ids: Vec<RecordId> = if let Some(ref des) = from_designator {
1479 let mut found = Vec::new();
1481 for (id, record) in tree.iter() {
1482 if matches!(record, SchRecord::Component(_)) {
1483 if let Some(comp_des) = get_component_designator(&tree, id) {
1484 if comp_des.eq_ignore_ascii_case(des) {
1485 found.push(id);
1486 break;
1487 }
1488 }
1489 }
1490 }
1491 if found.is_empty() {
1492 return Err(format!("Component '{}' not found", des).into());
1493 }
1494 found
1495 } else {
1496 tree.roots().map(|(id, _)| id).collect()
1498 };
1499
1500 let max_d = max_depth.unwrap_or(10);
1501 let hierarchy_nodes: Vec<HierarchyNode> = start_ids
1502 .into_iter()
1503 .map(|id| build_hierarchy_node(&tree, id, 0, max_d))
1504 .collect();
1505
1506 Ok(SchDocHierarchy {
1507 path: path.display().to_string(),
1508 hierarchy: hierarchy_nodes,
1509 })
1510}
1511
1512fn build_hierarchy_node(
1514 tree: &RecordTree<SchRecord>,
1515 id: RecordId,
1516 depth: usize,
1517 max_depth: usize,
1518) -> HierarchyNode {
1519 if depth <= max_depth {
1520 let record = match tree.get(id) {
1521 Some(r) => r,
1522 None => {
1523 return HierarchyNode {
1524 node_type: "error".to_string(),
1525 unique_id: "Invalid ID".to_string(),
1526 description: String::new(),
1527 children: Vec::new(),
1528 };
1529 }
1530 };
1531
1532 let (node_type, identifier, description) = match record {
1534 SchRecord::Component(c) => {
1535 let des = get_component_designator(tree, id).unwrap_or_default();
1536 ("component".to_string(), des, c.lib_reference.clone())
1537 }
1538 SchRecord::Pin(p) => ("pin".to_string(), p.designator.clone(), p.name.clone()),
1539 SchRecord::Parameter(p) => (
1540 "parameter".to_string(),
1541 p.name.clone(),
1542 p.label.text.clone(),
1543 ),
1544 SchRecord::Designator(d) => (
1545 "designator".to_string(),
1546 d.param.name.clone(),
1547 d.param.label.text.clone(),
1548 ),
1549 SchRecord::NetLabel(nl) => {
1550 ("netlabel".to_string(), nl.label.text.clone(), String::new())
1551 }
1552 SchRecord::Port(p) => (
1553 "port".to_string(),
1554 p.name.clone(),
1555 format!("{:?}", p.io_type),
1556 ),
1557 SchRecord::PowerObject(p) => (
1558 "power".to_string(),
1559 p.text.clone(),
1560 format!("{:?}", p.style),
1561 ),
1562 _ => (
1563 record_type_name(record).to_string(),
1564 format!("[{}]", id.index()),
1565 String::new(),
1566 ),
1567 };
1568
1569 let children: Vec<_> = tree
1571 .children(id)
1572 .map(|(child_id, _)| build_hierarchy_node(tree, child_id, depth + 1, max_depth))
1573 .collect();
1574
1575 HierarchyNode {
1576 node_type,
1577 unique_id: identifier,
1578 description,
1579 children,
1580 }
1581 } else {
1582 HierarchyNode {
1584 node_type: "...".to_string(),
1585 unique_id: format!("(depth limit {} reached)", max_depth),
1586 description: String::new(),
1587 children: Vec::new(),
1588 }
1589 }
1590}
1591
1592pub fn cmd_search(path: &Path, query: &str, limit: Option<usize>) -> Result<(), String> {
1594 let doc = open_schdoc(path)?;
1595 let tree = RecordTree::from_records(doc.primitives.clone());
1596
1597 let query_lower = query.to_lowercase();
1598 let max_results = limit.unwrap_or(50);
1599
1600 println!("Search Results: {}", path.display());
1601 println!("Query: \"{}\"", query);
1602 println!("═══════════════════════════════════════════════════════════════");
1603
1604 let mut results = Vec::new();
1605
1606 for (id, record) in tree.iter() {
1607 let matches = match record {
1608 SchRecord::Component(c) => {
1609 c.lib_reference.to_lowercase().contains(&query_lower)
1610 || c.component_description
1611 .to_lowercase()
1612 .contains(&query_lower)
1613 }
1614 SchRecord::Pin(p) => {
1615 p.name.to_lowercase().contains(&query_lower)
1616 || p.designator.to_lowercase().contains(&query_lower)
1617 }
1618 SchRecord::NetLabel(nl) => nl.label.text.to_lowercase().contains(&query_lower),
1619 SchRecord::Port(p) => p.name.to_lowercase().contains(&query_lower),
1620 SchRecord::PowerObject(p) => p.text.to_lowercase().contains(&query_lower),
1621 SchRecord::Label(l) => l.text.to_lowercase().contains(&query_lower),
1622 SchRecord::TextFrame(tf) => tf.text.to_lowercase().contains(&query_lower),
1623 SchRecord::Parameter(p) => {
1624 p.name.to_lowercase().contains(&query_lower)
1625 || p.label.text.to_lowercase().contains(&query_lower)
1626 }
1627 SchRecord::Designator(d) => {
1628 d.param.name.to_lowercase().contains(&query_lower)
1629 || d.param.label.text.to_lowercase().contains(&query_lower)
1630 }
1631 _ => false,
1632 };
1633
1634 if matches {
1635 results.push((id, record));
1636 if results.len() >= max_results {
1637 break;
1638 }
1639 }
1640 }
1641
1642 println!("\nFound {} results:\n", results.len());
1643
1644 for (id, record) in &results {
1645 let desc = match record {
1646 SchRecord::Component(c) => {
1647 let des = get_component_designator(&tree, *id).unwrap_or_default();
1648 format!("Component {} - {}", des, c.lib_reference)
1649 }
1650 SchRecord::Pin(p) => format!("Pin {} - {}", p.designator, p.name),
1651 SchRecord::NetLabel(nl) => format!("NetLabel: {}", nl.label.text),
1652 SchRecord::Port(p) => format!("Port: {}", p.name),
1653 SchRecord::PowerObject(p) => format!("Power: {}", p.text),
1654 SchRecord::Label(l) => format!("Label: {}", l.text),
1655 SchRecord::TextFrame(tf) => {
1656 let text = if tf.text.len() > 40 {
1657 format!("{}...", &tf.text[..40])
1658 } else {
1659 tf.text.clone()
1660 };
1661 format!("TextFrame: {}", text)
1662 }
1663 SchRecord::Parameter(p) => format!("Parameter: {} = {}", p.name, p.label.text),
1664 SchRecord::Designator(d) => {
1665 format!("Designator: {} = {}", d.param.name, d.param.label.text)
1666 }
1667 _ => record_type_name(record).to_string(),
1668 };
1669 println!(" [{}] {}", id.index(), desc);
1670 }
1671
1672 if results.len() >= max_results {
1673 println!("\n(results limited to {})", max_results);
1674 }
1675
1676 Ok(())
1677}
1678
1679pub fn cmd_junctions(path: &Path) -> Result<SchDocJunctionList, Box<dyn std::error::Error>> {
1681 let doc = open_schdoc(path)?;
1682
1683 let junctions: Vec<JunctionInfo> = doc
1684 .primitives
1685 .iter()
1686 .filter_map(|r| {
1687 if let SchRecord::Junction(j) = r {
1688 Some(JunctionInfo {
1689 location: fmt_point(j.graphical.location_x, j.graphical.location_y),
1690 })
1691 } else {
1692 None
1693 }
1694 })
1695 .collect();
1696
1697 Ok(SchDocJunctionList {
1698 path: path.display().to_string(),
1699 total_junctions: junctions.len(),
1700 junctions,
1701 })
1702}
1703
1704#[derive(Serialize)]
1706struct JsonDocument {
1707 file: String,
1708 sheet: Option<JsonSheet>,
1709 summary: JsonSummary,
1710 #[serde(skip_serializing_if = "Option::is_none")]
1711 components: Option<Vec<JsonComponent>>,
1712 #[serde(skip_serializing_if = "Option::is_none")]
1713 nets: Option<Vec<JsonNet>>,
1714 #[serde(skip_serializing_if = "Option::is_none")]
1715 ports: Option<Vec<JsonPort>>,
1716 #[serde(skip_serializing_if = "Option::is_none")]
1717 power: Option<Vec<JsonPower>>,
1718}
1719
1720#[derive(Serialize)]
1721struct JsonSheet {
1722 size: String,
1723 fonts: i32,
1724}
1725
1726#[derive(Serialize)]
1727struct JsonSummary {
1728 total_primitives: usize,
1729 components: usize,
1730 wires: usize,
1731 net_labels: usize,
1732 ports: usize,
1733 power_objects: usize,
1734 junctions: usize,
1735 pins: usize,
1736}
1737
1738#[derive(Serialize)]
1739struct JsonComponent {
1740 designator: String,
1741 lib_reference: String,
1742 description: String,
1743 location: String,
1744 pins: Vec<JsonPin>,
1745 parameters: Vec<JsonParameter>,
1746}
1747
1748#[derive(Serialize)]
1749struct JsonPin {
1750 designator: String,
1751 name: String,
1752 electrical: String,
1753 hidden: bool,
1754}
1755
1756#[derive(Serialize)]
1757struct JsonParameter {
1758 name: String,
1759 value: String,
1760}
1761
1762#[derive(Serialize)]
1763struct JsonNet {
1764 name: String,
1765 location: String,
1766}
1767
1768#[derive(Serialize)]
1769struct JsonPort {
1770 name: String,
1771 io_type: String,
1772 location: String,
1773}
1774
1775#[derive(Serialize)]
1776struct JsonPower {
1777 net: String,
1778 style: String,
1779 location: String,
1780}
1781
1782pub fn cmd_json(path: &Path, full: bool, pretty: bool) -> Result<(), String> {
1784 let doc = open_schdoc(path)?;
1785 let tree = RecordTree::from_records(doc.primitives.clone());
1786 let counts = count_record_types(&doc);
1787
1788 let sheet = doc.sheet_header().map(|h| JsonSheet {
1790 size: sheet_size_name(h.sheet_size).to_string(),
1791 fonts: h.font_id_count,
1792 });
1793
1794 let summary = JsonSummary {
1796 total_primitives: doc.primitives.len(),
1797 components: counts.get("Component").copied().unwrap_or(0),
1798 wires: counts.get("Wire").copied().unwrap_or(0),
1799 net_labels: counts.get("NetLabel").copied().unwrap_or(0),
1800 ports: counts.get("Port").copied().unwrap_or(0),
1801 power_objects: counts.get("PowerObject").copied().unwrap_or(0),
1802 junctions: counts.get("Junction").copied().unwrap_or(0),
1803 pins: counts.get("Pin").copied().unwrap_or(0),
1804 };
1805
1806 let (components, nets, ports, power) = if full {
1808 let mut components = Vec::new();
1810 for (id, record) in tree.iter() {
1811 if let SchRecord::Component(c) = record {
1812 let des = get_component_designator(&tree, id).unwrap_or_default();
1813
1814 let mut pins = Vec::new();
1815 let mut params = Vec::new();
1816
1817 for (_, child) in tree.children(id) {
1818 match child {
1819 SchRecord::Pin(p) => {
1820 pins.push(JsonPin {
1821 designator: p.designator.clone(),
1822 name: p.name.clone(),
1823 electrical: format!("{:?}", p.electrical),
1824 hidden: p.is_hidden(),
1825 });
1826 }
1827 SchRecord::Parameter(p) => {
1828 params.push(JsonParameter {
1829 name: p.name.clone(),
1830 value: p.label.text.clone(),
1831 });
1832 }
1833 _ => {}
1834 }
1835 }
1836
1837 components.push(JsonComponent {
1838 designator: des,
1839 lib_reference: c.lib_reference.clone(),
1840 description: c.component_description.clone(),
1841 location: fmt_point(c.graphical.location_x, c.graphical.location_y),
1842 pins,
1843 parameters: params,
1844 });
1845 }
1846 }
1847
1848 let nets: Vec<JsonNet> = doc
1850 .primitives
1851 .iter()
1852 .filter_map(|r| {
1853 if let SchRecord::NetLabel(nl) = r {
1854 Some(JsonNet {
1855 name: nl.label.text.clone(),
1856 location: fmt_point(
1857 nl.label.graphical.location_x,
1858 nl.label.graphical.location_y,
1859 ),
1860 })
1861 } else {
1862 None
1863 }
1864 })
1865 .collect();
1866
1867 let ports: Vec<JsonPort> = doc
1869 .primitives
1870 .iter()
1871 .filter_map(|r| {
1872 if let SchRecord::Port(p) = r {
1873 Some(JsonPort {
1874 name: p.name.clone(),
1875 io_type: format!("{:?}", p.io_type),
1876 location: fmt_point(p.graphical.location_x, p.graphical.location_y),
1877 })
1878 } else {
1879 None
1880 }
1881 })
1882 .collect();
1883
1884 let power: Vec<JsonPower> = doc
1886 .primitives
1887 .iter()
1888 .filter_map(|r| {
1889 if let SchRecord::PowerObject(p) = r {
1890 Some(JsonPower {
1891 net: p.text.clone(),
1892 style: format!("{:?}", p.style),
1893 location: fmt_point(p.graphical.location_x, p.graphical.location_y),
1894 })
1895 } else {
1896 None
1897 }
1898 })
1899 .collect();
1900
1901 (Some(components), Some(nets), Some(ports), Some(power))
1902 } else {
1903 (None, None, None, None)
1904 };
1905
1906 let json_doc = JsonDocument {
1907 file: path.display().to_string(),
1908 sheet,
1909 summary,
1910 components,
1911 nets,
1912 ports,
1913 power,
1914 };
1915
1916 let output = if pretty {
1917 serde_json::to_string_pretty(&json_doc)
1918 } else {
1919 serde_json::to_string(&json_doc)
1920 }
1921 .map_err(|e| format!("JSON serialization error: {}", e))?;
1922
1923 println!("{}", output);
1924
1925 Ok(())
1926}
1927
1928pub use crate::ops::schdoc_edit::{
1933 cmd_add_component, cmd_add_junction, cmd_add_missing_junctions, cmd_add_net_label,
1934 cmd_add_port, cmd_add_power, cmd_add_wire, cmd_connect_pins, cmd_delete_component,
1935 cmd_delete_wire, cmd_find_missing_junctions, cmd_find_unconnected, cmd_list_library,
1936 cmd_move_component, cmd_new, cmd_route_wire, cmd_search_library, cmd_show_netlist,
1937 cmd_suggest_placement, cmd_validate,
1938};