1use std::collections::HashMap;
11use std::fs::File;
12use std::io::{BufReader, Cursor};
13use std::path::Path;
14
15use serde::{Deserialize, Serialize};
16use serde_json;
17
18use super::util::alphanumeric_sort;
19use crate::dump::fmt_coord;
20use crate::io::{SchLib, SchLibComponent};
21use crate::ops::categorization::categorize_component;
22use crate::ops::output::*;
23use crate::records::sch::{
24 LineWidth, PinConglomerateFlags, PinElectricalType, PinSymbol, SchArc, SchComponent,
25 SchEllipse, SchGraphicalBase, SchLabel, SchLine, SchPin, SchPolygon, SchPolyline, SchRecord,
26 SchRectangle, TextJustification, TextOrientations,
27};
28use crate::types::Unit;
29
30fn open_schlib(path: &Path) -> Result<SchLib, Box<dyn std::error::Error>> {
31 let file = File::open(path)?;
32 Ok(SchLib::open(BufReader::new(file))?)
33}
34
35fn electrical_type_name(et: &PinElectricalType) -> &'static str {
37 match et {
38 PinElectricalType::Input => "Input",
39 PinElectricalType::InputOutput => "I/O",
40 PinElectricalType::Output => "Output",
41 PinElectricalType::OpenCollector => "Open Collector",
42 PinElectricalType::Passive => "Passive",
43 PinElectricalType::HiZ => "Hi-Z",
44 PinElectricalType::OpenEmitter => "Open Emitter",
45 PinElectricalType::Power => "Power",
46 }
47}
48
49fn record_type_name(record: &SchRecord) -> &'static str {
51 match record {
52 SchRecord::Component(_) => "Component",
53 SchRecord::Pin(_) => "Pin",
54 SchRecord::Symbol(_) => "Symbol",
55 SchRecord::Label(_) => "Label",
56 SchRecord::Bezier(_) => "Bezier",
57 SchRecord::Polyline(_) => "Polyline",
58 SchRecord::Polygon(_) => "Polygon",
59 SchRecord::Ellipse(_) => "Ellipse",
60 SchRecord::Pie(_) => "Pie",
61 SchRecord::EllipticalArc(_) => "EllipticalArc",
62 SchRecord::Arc(_) => "Arc",
63 SchRecord::Line(_) => "Line",
64 SchRecord::Rectangle(_) => "Rectangle",
65 SchRecord::PowerObject(_) => "PowerObject",
66 SchRecord::Port(_) => "Port",
67 SchRecord::NoErc(_) => "NoERC",
68 SchRecord::NetLabel(_) => "NetLabel",
69 SchRecord::Bus(_) => "Bus",
70 SchRecord::Wire(_) => "Wire",
71 SchRecord::TextFrame(_) => "TextFrame",
72 SchRecord::TextFrameVariant(_) => "TextFrameVariant",
73 SchRecord::Junction(_) => "Junction",
74 SchRecord::Image(_) => "Image",
75 SchRecord::SheetHeader(_) => "SheetHeader",
76 SchRecord::Designator(_) => "Designator",
77 SchRecord::BusEntry(_) => "BusEntry",
78 SchRecord::Parameter(_) => "Parameter",
79 SchRecord::WarningSign(_) => "WarningSign",
80 SchRecord::ImplementationList(_) => "ImplementationList",
81 SchRecord::Implementation(_) => "Implementation",
82 SchRecord::MapDefinerList(_) => "MapDefinerList",
83 SchRecord::MapDefiner(_) => "MapDefiner",
84 SchRecord::ImplementationParameters(_) => "ImplementationParameters",
85 SchRecord::Unknown { .. } => "Unknown",
86 }
87}
88
89pub fn cmd_overview(path: &Path, full: bool) -> Result<SchLibOverview, Box<dyn std::error::Error>> {
95 let lib = open_schlib(path)?;
96
97 let mut categories: HashMap<&'static str, Vec<ComponentSummary>> = HashMap::new();
101
102 for comp in lib.iter() {
103 let category = categorize_component(
104 &comp.component.lib_reference,
105 &comp.component.component_description,
106 );
107 categories
108 .entry(category)
109 .or_default()
110 .push(ComponentSummary {
111 name: comp.component.lib_reference.clone(),
112 description: comp.component.component_description.clone(),
113 pin_count: comp.pin_count(),
114 part_count: comp.component.part_count,
115 });
116 }
117
118 let category_order = [
120 "Microcontroller",
121 "FPGA/CPLD",
122 "Memory",
123 "ADC",
124 "DAC",
125 "Transceiver/PHY",
126 "Clock/Oscillator",
127 "Power Supply",
128 "Amplifier",
129 "Mux/Switch",
130 "Buffer/Driver",
131 "Other IC",
132 "Transistor",
133 "Diode/Protection",
134 "LED",
135 "Capacitor",
136 "Resistor",
137 "Inductor/Ferrite",
138 "Connector",
139 "Test Point",
140 ];
141
142 let mut components_by_category = Vec::new();
143 for category in category_order.iter() {
144 if let Some(comps) = categories.remove(*category) {
145 components_by_category.push((category.to_string(), comps));
146 }
147 }
148
149 for (category, comps) in categories {
151 if !comps.is_empty() {
152 components_by_category.push((category.to_string(), comps));
153 }
154 }
155
156 let mut total_pins = 0;
160 let mut pin_types: HashMap<String, usize> = HashMap::new();
161
162 for comp in lib.iter() {
163 for prim in &comp.primitives {
164 if let SchRecord::Pin(pin) = prim {
165 total_pins += 1;
166 *pin_types
167 .entry(electrical_type_name(&pin.electrical).to_string())
168 .or_insert(0) += 1;
169 }
170 }
171 }
172
173 let mut sorted_types: Vec<_> = pin_types.into_iter().collect();
174 sorted_types.sort_by(|a, b| b.1.cmp(&a.1));
175
176 let pin_statistics = PinStatistics {
177 total_pins,
178 pin_types: sorted_types,
179 };
180
181 let multi_part_components: Vec<ComponentSummary> = lib
185 .iter()
186 .filter(|c| c.component.part_count > 1)
187 .map(|comp| ComponentSummary {
188 name: comp.component.lib_reference.clone(),
189 description: comp.component.component_description.clone(),
190 pin_count: comp.pin_count(),
191 part_count: comp.component.part_count,
192 })
193 .collect();
194
195 let mut largest_components: Vec<ComponentSummary> = lib
199 .iter()
200 .map(|comp| ComponentSummary {
201 name: comp.component.lib_reference.clone(),
202 description: comp.component.component_description.clone(),
203 pin_count: comp.pin_count(),
204 part_count: comp.component.part_count,
205 })
206 .collect();
207 largest_components.sort_by(|a, b| b.pin_count.cmp(&a.pin_count));
208 largest_components.truncate(10);
209
210 let component_details = if full {
214 Some(
215 lib.iter()
216 .map(|comp| {
217 let pins = comp
218 .primitives
219 .iter()
220 .filter_map(|prim| {
221 if let SchRecord::Pin(pin) = prim {
222 Some(PinDetail {
223 designator: pin.designator.clone(),
224 name: pin.name.clone(),
225 electrical_type: electrical_type_name(&pin.electrical)
226 .to_string(),
227 description: pin.description.clone(),
228 })
229 } else {
230 None
231 }
232 })
233 .collect();
234
235 SchLibComponentDetail {
236 name: comp.component.lib_reference.clone(),
237 description: comp.component.component_description.clone(),
238 part_count: comp.component.part_count,
239 display_mode_count: comp.component.display_mode_count,
240 pin_count: comp.pin_count(),
241 total_primitives: comp.primitives.len(),
242 pins,
243 primitive_counts: None,
244 }
245 })
246 .collect(),
247 )
248 } else {
249 None
250 };
251
252 Ok(SchLibOverview {
253 path: path.display().to_string(),
254 total_components: lib.components.len(),
255 components_by_category,
256 pin_statistics,
257 multi_part_components,
258 largest_components,
259 component_details,
260 })
261}
262
263pub fn cmd_list(path: &Path) -> Result<SchLibComponentList, Box<dyn std::error::Error>> {
265 let lib = open_schlib(path)?;
266
267 let components: Vec<ComponentSummary> = lib
268 .iter()
269 .map(|comp| ComponentSummary {
270 name: comp.component.lib_reference.clone(),
271 description: comp.component.component_description.clone(),
272 pin_count: comp.pin_count(),
273 part_count: comp.component.part_count,
274 })
275 .collect();
276
277 Ok(SchLibComponentList {
278 path: path.display().to_string(),
279 total_components: lib.components.len(),
280 components,
281 })
282}
283
284pub fn cmd_search(
286 path: &Path,
287 query: &str,
288 limit: Option<usize>,
289) -> Result<SchLibSearchResults, Box<dyn std::error::Error>> {
290 let lib = open_schlib(path)?;
291
292 let query_lower = query.to_lowercase();
293 let has_wildcard = query.contains('*');
294
295 let matches: Vec<ComponentSummary> = lib
296 .iter()
297 .filter(|comp| {
298 let name = comp.component.lib_reference.to_lowercase();
299 let desc = comp.component.component_description.to_lowercase();
300
301 if has_wildcard {
302 let pattern = query_lower.replace('*', "");
303 name.contains(&pattern) || desc.contains(&pattern)
304 } else {
305 name.contains(&query_lower) || desc.contains(&query_lower)
306 }
307 })
308 .map(|comp| ComponentSummary {
309 name: comp.component.lib_reference.clone(),
310 description: comp.component.component_description.clone(),
311 pin_count: comp.pin_count(),
312 part_count: comp.component.part_count,
313 })
314 .collect();
315
316 let total_matches = matches.len();
317 let results = if let Some(limit) = limit {
318 matches.into_iter().take(limit).collect()
319 } else {
320 matches
321 };
322
323 Ok(SchLibSearchResults {
324 query: query.to_string(),
325 total_matches,
326 results,
327 })
328}
329
330pub fn cmd_info(path: &Path) -> Result<SchLibInfo, Box<dyn std::error::Error>> {
336 let lib = open_schlib(path)?;
337
338 let mut primitive_counts: HashMap<String, usize> = HashMap::new();
340 let mut total_primitives = 0;
341
342 for comp in lib.iter() {
343 for prim in &comp.primitives {
344 let name = record_type_name(prim).to_string();
345 *primitive_counts.entry(name).or_insert(0) += 1;
346 total_primitives += 1;
347 }
348 }
349
350 let mut sorted: Vec<_> = primitive_counts.into_iter().collect();
351 sorted.sort_by(|a, b| b.1.cmp(&a.1));
352
353 let multi_part_count = lib.iter().filter(|c| c.component.part_count > 1).count();
355
356 Ok(SchLibInfo {
357 path: path.display().to_string(),
358 component_count: lib.components.len(),
359 total_primitives,
360 primitive_types: sorted,
361 multi_part_count,
362 })
363}
364
365pub fn cmd_component(
367 path: &Path,
368 name: &str,
369 show_primitives: bool,
370) -> Result<SchLibComponentDetail, Box<dyn std::error::Error>> {
371 let lib = open_schlib(path)?;
372
373 let name_lower = name.to_lowercase();
374 let comp = lib
375 .iter()
376 .find(|c| c.component.lib_reference.to_lowercase() == name_lower)
377 .ok_or_else(|| format!("Component '{}' not found", name))?;
378
379 let mut pins: Vec<&SchPin> = comp
381 .primitives
382 .iter()
383 .filter_map(|p| {
384 if let SchRecord::Pin(pin) = p {
385 Some(pin)
386 } else {
387 None
388 }
389 })
390 .collect();
391
392 pins.sort_by(|a, b| alphanumeric_sort(&a.designator, &b.designator));
393
394 let pins_detail: Vec<PinDetail> = pins
395 .iter()
396 .map(|pin| PinDetail {
397 designator: pin.designator.clone(),
398 name: pin.name.clone(),
399 electrical_type: electrical_type_name(&pin.electrical).to_string(),
400 description: pin.description.clone(),
401 })
402 .collect();
403
404 let primitive_counts = if show_primitives {
405 let mut prim_counts: HashMap<String, usize> = HashMap::new();
406 for prim in &comp.primitives {
407 *prim_counts
408 .entry(record_type_name(prim).to_string())
409 .or_insert(0) += 1;
410 }
411 Some(prim_counts.into_iter().collect())
412 } else {
413 None
414 };
415
416 Ok(SchLibComponentDetail {
417 name: comp.component.lib_reference.clone(),
418 description: comp.component.component_description.clone(),
419 part_count: comp.component.part_count,
420 display_mode_count: comp.component.display_mode_count,
421 pin_count: comp.pin_count(),
422 total_primitives: comp.primitive_count(),
423 pins: pins_detail,
424 primitive_counts,
425 })
426}
427
428pub fn cmd_pins(
430 path: &Path,
431 component_filter: Option<String>,
432 by_type: bool,
433) -> Result<SchLibPinList, Box<dyn std::error::Error>> {
434 let lib = open_schlib(path)?;
435
436 let filter_lower = component_filter.as_ref().map(|s| s.to_lowercase());
437
438 let mut all_pins: Vec<PinWithComponent> = Vec::new();
439
440 for comp in lib.iter() {
441 if let Some(ref filter) = filter_lower {
442 if !comp.component.lib_reference.to_lowercase().contains(filter) {
443 continue;
444 }
445 }
446
447 for prim in &comp.primitives {
448 if let SchRecord::Pin(pin) = prim {
449 all_pins.push(PinWithComponent {
450 component_name: comp.component.lib_reference.clone(),
451 designator: pin.designator.clone(),
452 name: pin.name.clone(),
453 electrical_type: electrical_type_name(&pin.electrical).to_string(),
454 });
455 }
456 }
457 }
458
459 let pins_by_type = if by_type {
460 let mut by_type: HashMap<String, Vec<PinWithComponent>> = HashMap::new();
461 for pin in &all_pins {
462 by_type
463 .entry(pin.electrical_type.clone())
464 .or_default()
465 .push(pin.clone());
466 }
467
468 let type_order = [
469 "Input",
470 "Output",
471 "I/O",
472 "Passive",
473 "Power",
474 "Open Collector",
475 "Open Emitter",
476 "Hi-Z",
477 ];
478 let mut ordered: Vec<(String, Vec<PinWithComponent>)> = Vec::new();
479
480 for etype in type_order {
481 if let Some(pins) = by_type.remove(etype) {
482 ordered.push((etype.to_string(), pins));
483 }
484 }
485
486 for (etype, pins) in by_type {
488 ordered.push((etype, pins));
489 }
490
491 Some(ordered)
492 } else {
493 None
494 };
495
496 Ok(SchLibPinList {
497 path: path.display().to_string(),
498 total_pins: all_pins.len(),
499 pins: all_pins,
500 pins_by_type,
501 })
502}
503
504pub fn cmd_primitives(
506 path: &Path,
507 name: &str,
508) -> Result<SchLibPrimitiveList, Box<dyn std::error::Error>> {
509 let lib = open_schlib(path)?;
510
511 let name_lower = name.to_lowercase();
512 let comp = lib
513 .iter()
514 .find(|c| c.component.lib_reference.to_lowercase() == name_lower)
515 .ok_or_else(|| format!("Component '{}' not found", name))?;
516
517 let primitives: Vec<PrimitiveInfo> = comp
519 .primitives
520 .iter()
521 .skip(1)
522 .map(|prim| match prim {
523 SchRecord::Pin(p) => PrimitiveInfo::Pin {
524 designator: p.designator.clone(),
525 name: p.name.clone(),
526 electrical_type: electrical_type_name(&p.electrical).to_string(),
527 x: fmt_coord(p.graphical.location_x),
528 y: fmt_coord(p.graphical.location_y),
529 },
530 SchRecord::Rectangle(r) => PrimitiveInfo::Rectangle {
531 x1: fmt_coord(r.graphical.location_x),
532 y1: fmt_coord(r.graphical.location_y),
533 x2: fmt_coord(r.corner_x),
534 y2: fmt_coord(r.corner_y),
535 },
536 SchRecord::Line(l) => PrimitiveInfo::Line {
537 x1: fmt_coord(l.graphical.location_x),
538 y1: fmt_coord(l.graphical.location_y),
539 x2: fmt_coord(l.corner_x),
540 y2: fmt_coord(l.corner_y),
541 },
542 SchRecord::Arc(a) => PrimitiveInfo::Arc {
543 center_x: fmt_coord(a.graphical.location_x),
544 center_y: fmt_coord(a.graphical.location_y),
545 radius: fmt_coord(a.radius),
546 start_angle: a.start_angle,
547 end_angle: a.end_angle,
548 },
549 SchRecord::Polygon(p) => PrimitiveInfo::Polygon {
550 vertex_count: p.vertices.len(),
551 },
552 SchRecord::Polyline(p) => PrimitiveInfo::Polyline {
553 vertex_count: p.vertices.len(),
554 },
555 SchRecord::Label(l) => PrimitiveInfo::Label {
556 text: l.text.clone(),
557 x: fmt_coord(l.graphical.location_x),
558 y: fmt_coord(l.graphical.location_y),
559 },
560 _ => PrimitiveInfo::Other {
561 primitive_type: record_type_name(prim).to_string(),
562 },
563 })
564 .collect();
565
566 Ok(SchLibPrimitiveList {
567 component_name: comp.component.lib_reference.clone(),
568 total_primitives: comp.primitive_count(),
569 primitives,
570 })
571}
572
573pub fn cmd_json(path: &Path) -> Result<SchLibComponentList, Box<dyn std::error::Error>> {
575 cmd_list(path)
576}
577
578const BLANK_SCHLIB_TEMPLATE: &[u8] = include_bytes!("../../data/blank/Schlib1.SchLib");
584
585pub fn cmd_create(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
587 if path.exists() {
588 return Err(format!("File already exists: {}", path.display()).into());
589 }
590
591 std::fs::write(path, BLANK_SCHLIB_TEMPLATE)?;
592
593 Ok(format!("Created empty SchLib: {}", path.display()))
594}
595
596fn load_blank_schlib() -> Result<SchLib, Box<dyn std::error::Error>> {
597 Ok(SchLib::open(Cursor::new(BLANK_SCHLIB_TEMPLATE))?)
598}
599
600fn open_or_create_schlib(path: &Path) -> Result<SchLib, Box<dyn std::error::Error>> {
602 if path.exists() {
603 open_schlib(path)
604 } else {
605 load_blank_schlib()
606 }
607}
608
609fn save_schlib(path: &Path, lib: &SchLib) -> Result<(), Box<dyn std::error::Error>> {
611 Ok(lib.save_to_file(path)?)
612}
613
614fn parse_color(hex: &str) -> Result<i32, Box<dyn std::error::Error>> {
616 let hex = hex.trim_start_matches('#');
618
619 if hex.len() != 6 {
620 return Err(format!(
621 "Invalid color format: {}. Expected 6 hex digits (RRGGBB)",
622 hex
623 )
624 .into());
625 }
626
627 let r = u8::from_str_radix(&hex[0..2], 16)
628 .map_err(|_| format!("Invalid red component in color: {}", hex))?;
629 let g = u8::from_str_radix(&hex[2..4], 16)
630 .map_err(|_| format!("Invalid green component in color: {}", hex))?;
631 let b = u8::from_str_radix(&hex[4..6], 16)
632 .map_err(|_| format!("Invalid blue component in color: {}", hex))?;
633
634 Ok((b as i32) << 16 | (g as i32) << 8 | (r as i32))
636}
637
638fn parse_electrical_type(s: &str) -> Result<PinElectricalType, Box<dyn std::error::Error>> {
640 match s.to_lowercase().as_str() {
641 "input" | "in" => Ok(PinElectricalType::Input),
642 "output" | "out" => Ok(PinElectricalType::Output),
643 "io" | "inputoutput" | "bidirectional" | "bidir" => Ok(PinElectricalType::InputOutput),
644 "passive" | "pas" => Ok(PinElectricalType::Passive),
645 "power" | "pwr" => Ok(PinElectricalType::Power),
646 "oc" | "opencollector" => Ok(PinElectricalType::OpenCollector),
647 "oe" | "openemitter" => Ok(PinElectricalType::OpenEmitter),
648 "hiz" | "tristate" | "3state" => Ok(PinElectricalType::HiZ),
649 _ => Err(format!(
650 "Unknown electrical type: {}. Use: input, output, io, passive, power, oc, oe, hiz",
651 s
652 )
653 .into()),
654 }
655}
656
657fn parse_pin_orientation(s: &str) -> Result<PinConglomerateFlags, Box<dyn std::error::Error>> {
659 match s.to_lowercase().as_str() {
660 "right" => Ok(PinConglomerateFlags::empty()), "left" => Ok(PinConglomerateFlags::FLIPPED), "up" => Ok(PinConglomerateFlags::ROTATED), "down" => Ok(PinConglomerateFlags::ROTATED | PinConglomerateFlags::FLIPPED), _ => Err(format!("Unknown orientation: {}. Use: left, right, up, down", s).into()),
665 }
666}
667
668fn mils_to_raw(mils: i32) -> i32 {
670 mils * 10000
671}
672
673fn mils_f64_to_raw(mils: f64) -> i32 {
675 (mils * 10000.0).round() as i32
676}
677
678#[allow(dead_code)] fn parse_unit_value_to_mils(s: &str) -> Result<f64, Box<dyn std::error::Error>> {
686 let (coord, _unit) =
687 Unit::parse_with_unit(s).map_err(|e| format!("Invalid value '{}': {:?}", s, e))?;
688 Ok(coord.to_mils())
689}
690
691fn parse_unit_value_or_mil(s: &str) -> Result<f64, Box<dyn std::error::Error>> {
694 let s = s.trim();
695
696 if let Ok((coord, _unit)) = Unit::parse_with_unit(s) {
698 return Ok(coord.to_mils());
699 }
700
701 s.parse::<f64>().map_err(|_| {
703 format!(
704 "Invalid value '{}': expected number with optional unit (e.g., '100mil', '2.54mm')",
705 s
706 )
707 .into()
708 })
709}
710
711#[derive(Debug, Clone)]
718pub struct CoordValue(pub f64);
719
720impl CoordValue {
721 pub fn to_mils(&self) -> f64 {
723 self.0
724 }
725
726 pub fn to_raw(&self) -> i32 {
728 mils_f64_to_raw(self.0)
729 }
730}
731
732impl Default for CoordValue {
733 fn default() -> Self {
734 CoordValue(0.0)
735 }
736}
737
738impl<'de> Deserialize<'de> for CoordValue {
739 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
740 where
741 D: serde::Deserializer<'de>,
742 {
743 use serde::de::{self, Visitor};
744
745 struct CoordValueVisitor;
746
747 impl<'de> Visitor<'de> for CoordValueVisitor {
748 type Value = CoordValue;
749
750 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
751 formatter.write_str(
752 "a number (mils) or a string with unit (e.g., \"100mil\", \"2.54mm\")",
753 )
754 }
755
756 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
757 where
758 E: de::Error,
759 {
760 Ok(CoordValue(value as f64))
761 }
762
763 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
764 where
765 E: de::Error,
766 {
767 Ok(CoordValue(value as f64))
768 }
769
770 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
771 where
772 E: de::Error,
773 {
774 Ok(CoordValue(value))
775 }
776
777 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
778 where
779 E: de::Error,
780 {
781 parse_unit_value_or_mil(value)
782 .map(CoordValue)
783 .map_err(de::Error::custom)
784 }
785 }
786
787 deserializer.deserialize_any(CoordValueVisitor)
788 }
789}
790
791impl Serialize for CoordValue {
792 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
793 where
794 S: serde::Serializer,
795 {
796 serializer.serialize_f64(self.0)
798 }
799}
800
801pub fn cmd_add_component(
803 path: &Path,
804 name: &str,
805 description: Option<String>,
806) -> Result<String, Box<dyn std::error::Error>> {
807 let mut lib = open_or_create_schlib(path)?;
808
809 if lib
811 .components
812 .iter()
813 .any(|c| c.component.lib_reference == name)
814 {
815 return Err(format!("Component '{}' already exists", name).into());
816 }
817
818 let component = SchComponent {
820 lib_reference: name.to_string(),
821 component_description: description.unwrap_or_default(),
822 part_count: 1,
823 display_mode_count: 1,
824 current_part_id: 1,
825 ..Default::default()
826 };
827
828 let lib_component = SchLibComponent {
830 component: component.clone(),
831 primitives: vec![SchRecord::Component(component)],
832 };
833
834 lib.components.push(lib_component);
835 save_schlib(path, &lib)?;
836
837 Ok(format!("Added component '{}' to {}", name, path.display()))
838}
839
840#[allow(clippy::too_many_arguments)]
842pub fn cmd_add_pin(
843 path: &Path,
844 component_name: &str,
845 designator: &str,
846 name: &str,
847 x: &str,
848 y: &str,
849 length: &str,
850 electrical: &str,
851 orientation: &str,
852 hidden: bool,
853) -> Result<String, Box<dyn std::error::Error>> {
854 let mut lib = open_schlib(path)?;
855
856 let x_mils = parse_unit_value_or_mil(x)?;
858 let y_mils = parse_unit_value_or_mil(y)?;
859 let length_mils = parse_unit_value_or_mil(length)?;
860
861 let component = lib
863 .components
864 .iter_mut()
865 .find(|c| c.component.lib_reference == component_name)
866 .ok_or_else(|| format!("Component '{}' not found", component_name))?;
867
868 let electrical_type = parse_electrical_type(electrical)?;
870 let mut conglomerate = parse_pin_orientation(orientation)?;
871
872 conglomerate |= PinConglomerateFlags::DISPLAY_NAME_VISIBLE;
874 conglomerate |= PinConglomerateFlags::DESIGNATOR_VISIBLE;
875
876 if hidden {
877 conglomerate |= PinConglomerateFlags::HIDE;
878 }
879
880 let mut graphical = SchGraphicalBase::default();
882 graphical.base.owner_part_id = Some(1);
883 graphical.location_x = mils_f64_to_raw(x_mils);
884 graphical.location_y = mils_f64_to_raw(y_mils);
885 graphical.color = 0x000080; let pin = SchPin {
888 graphical,
889 designator: designator.to_string(),
890 name: name.to_string(),
891 electrical: electrical_type,
892 pin_conglomerate: conglomerate,
893 pin_length: mils_f64_to_raw(length_mils),
894 symbol_inner_edge: PinSymbol::None,
895 symbol_outer_edge: PinSymbol::None,
896 symbol_inside: PinSymbol::None,
897 symbol_outside: PinSymbol::None,
898 ..Default::default()
899 };
900
901 component.primitives.push(SchRecord::Pin(pin));
902 save_schlib(path, &lib)?;
903
904 Ok(format!(
905 "Added pin '{}' ({}) to component '{}'",
906 designator, name, component_name
907 ))
908}
909
910#[allow(clippy::too_many_arguments)]
912pub fn cmd_add_rectangle(
913 path: &Path,
914 component_name: &str,
915 x1: &str,
916 y1: &str,
917 x2: &str,
918 y2: &str,
919 filled: bool,
920 fill_color: &str,
921 border_color: &str,
922) -> Result<String, Box<dyn std::error::Error>> {
923 let mut lib = open_schlib(path)?;
924
925 let x1_mils = parse_unit_value_or_mil(x1)?;
927 let y1_mils = parse_unit_value_or_mil(y1)?;
928 let x2_mils = parse_unit_value_or_mil(x2)?;
929 let y2_mils = parse_unit_value_or_mil(y2)?;
930
931 let component = lib
933 .components
934 .iter_mut()
935 .find(|c| c.component.lib_reference == component_name)
936 .ok_or_else(|| format!("Component '{}' not found", component_name))?;
937
938 let fill_color_val = parse_color(fill_color)?;
940 let border_color_val = parse_color(border_color)?;
941
942 let mut graphical = SchGraphicalBase::default();
944 graphical.base.owner_part_id = Some(1);
945 graphical.location_x = mils_f64_to_raw(x1_mils);
946 graphical.location_y = mils_f64_to_raw(y1_mils);
947 graphical.color = border_color_val;
948 graphical.area_color = fill_color_val;
949
950 let rect = SchRectangle {
951 graphical,
952 corner_x: mils_f64_to_raw(x2_mils),
953 corner_y: mils_f64_to_raw(y2_mils),
954 line_width: LineWidth::Small,
955 is_solid: filled,
956 transparent: !filled,
957 ..Default::default()
958 };
959
960 component.primitives.push(SchRecord::Rectangle(rect));
961 save_schlib(path, &lib)?;
962
963 Ok(format!("Added rectangle to component '{}'", component_name))
964}
965
966pub fn cmd_add_line(
968 path: &Path,
969 component_name: &str,
970 x1: &str,
971 y1: &str,
972 x2: &str,
973 y2: &str,
974 color: &str,
975) -> Result<String, Box<dyn std::error::Error>> {
976 let mut lib = open_schlib(path)?;
977
978 let x1_mils = parse_unit_value_or_mil(x1)?;
980 let y1_mils = parse_unit_value_or_mil(y1)?;
981 let x2_mils = parse_unit_value_or_mil(x2)?;
982 let y2_mils = parse_unit_value_or_mil(y2)?;
983
984 let component = lib
986 .components
987 .iter_mut()
988 .find(|c| c.component.lib_reference == component_name)
989 .ok_or_else(|| format!("Component '{}' not found", component_name))?;
990
991 let color_val = parse_color(color)?;
993
994 let mut graphical = SchGraphicalBase::default();
996 graphical.base.owner_part_id = Some(1);
997 graphical.location_x = mils_f64_to_raw(x1_mils);
998 graphical.location_y = mils_f64_to_raw(y1_mils);
999 graphical.color = color_val;
1000
1001 let line = SchLine {
1002 graphical,
1003 corner_x: mils_f64_to_raw(x2_mils),
1004 corner_y: mils_f64_to_raw(y2_mils),
1005 line_width: LineWidth::Small,
1006 ..Default::default()
1007 };
1008
1009 component.primitives.push(SchRecord::Line(line));
1010 save_schlib(path, &lib)?;
1011
1012 Ok(format!("Added line to component '{}'", component_name))
1013}
1014
1015pub fn cmd_add_polygon(
1017 path: &Path,
1018 component_name: &str,
1019 vertices_str: &str,
1020 filled: bool,
1021 fill_color: &str,
1022 border_color: &str,
1023) -> Result<String, Box<dyn std::error::Error>> {
1024 let mut lib = open_schlib(path)?;
1025
1026 let component = lib
1028 .components
1029 .iter_mut()
1030 .find(|c| c.component.lib_reference == component_name)
1031 .ok_or_else(|| format!("Component '{}' not found", component_name))?;
1032
1033 let values: Vec<f64> = vertices_str
1035 .split(',')
1036 .map(|s| parse_unit_value_or_mil(s))
1037 .collect::<Result<Vec<_>, _>>()?;
1038
1039 if values.len() < 6 || values.len() % 2 != 0 {
1040 return Err("Need at least 3 vertex pairs (6 values)".into());
1041 }
1042
1043 let vertices: Vec<(i32, i32)> = values
1044 .chunks(2)
1045 .map(|chunk| (mils_f64_to_raw(chunk[0]), mils_f64_to_raw(chunk[1])))
1046 .collect();
1047
1048 let fill_color_val = parse_color(fill_color)?;
1050 let border_color_val = parse_color(border_color)?;
1051
1052 let mut graphical = SchGraphicalBase::default();
1054 graphical.base.owner_part_id = Some(1);
1055 graphical.location_x = vertices[0].0;
1056 graphical.location_y = vertices[0].1;
1057 graphical.color = border_color_val;
1058 graphical.area_color = fill_color_val;
1059
1060 let polygon = SchPolygon {
1061 graphical,
1062 vertices,
1063 line_width: LineWidth::Small,
1064 is_solid: filled,
1065 transparent: !filled,
1066 ..Default::default()
1067 };
1068
1069 component.primitives.push(SchRecord::Polygon(polygon));
1070 save_schlib(path, &lib)?;
1071
1072 Ok(format!(
1073 "Added polygon with {} vertices to component '{}'",
1074 values.len() / 2,
1075 component_name
1076 ))
1077}
1078
1079struct PinDef {
1081 designator: String,
1082 name: String,
1083 electrical: PinElectricalType,
1084 side: String,
1085}
1086
1087fn parse_pin_defs(pins_str: &str) -> Result<Vec<PinDef>, Box<dyn std::error::Error>> {
1089 let mut pins = Vec::new();
1090
1091 for pin_spec in pins_str.split(',') {
1092 let parts: Vec<&str> = pin_spec.trim().split(':').collect();
1093 if parts.len() < 3 {
1094 return Err(format!(
1095 "Invalid pin spec '{}'. Format: designator:name:type[:side]",
1096 pin_spec
1097 )
1098 .into());
1099 }
1100
1101 let electrical = parse_electrical_type(parts[2])?;
1102 let side = if parts.len() > 3 {
1103 parts[3].to_lowercase()
1104 } else {
1105 "left".to_string()
1106 };
1107
1108 pins.push(PinDef {
1109 designator: parts[0].to_string(),
1110 name: parts[1].to_string(),
1111 electrical,
1112 side,
1113 });
1114 }
1115
1116 Ok(pins)
1117}
1118
1119pub fn cmd_gen_ic(
1121 path: &Path,
1122 name: &str,
1123 pins_str: &str,
1124 description: Option<String>,
1125 width: &str,
1126 pin_length: &str,
1127 pin_spacing: &str,
1128) -> Result<String, Box<dyn std::error::Error>> {
1129 let mut lib = open_or_create_schlib(path)?;
1130
1131 let width_mils = parse_unit_value_or_mil(width)?;
1133 let pin_length_mils = parse_unit_value_or_mil(pin_length)?;
1134 let pin_spacing_mils = parse_unit_value_or_mil(pin_spacing)?;
1135
1136 if lib
1138 .components
1139 .iter()
1140 .any(|c| c.component.lib_reference == name)
1141 {
1142 return Err(format!("Component '{}' already exists", name).into());
1143 }
1144
1145 let pin_defs = parse_pin_defs(pins_str)?;
1147
1148 let left_pins: Vec<_> = pin_defs.iter().filter(|p| p.side == "left").collect();
1150 let right_pins: Vec<_> = pin_defs.iter().filter(|p| p.side == "right").collect();
1151 let top_pins: Vec<_> = pin_defs.iter().filter(|p| p.side == "top").collect();
1152 let bottom_pins: Vec<_> = pin_defs.iter().filter(|p| p.side == "bottom").collect();
1153
1154 let left_count = left_pins.len();
1156 let right_count = right_pins.len();
1157 let top_count = top_pins.len();
1158 let bottom_count = bottom_pins.len();
1159 let max_vertical_pins = left_count.max(right_count);
1160 let max_horizontal_pins = top_count.max(bottom_count);
1161 let body_height_mils = (max_vertical_pins + 1) as f64 * pin_spacing_mils;
1162 let min_width_for_tb = if max_horizontal_pins > 0 {
1164 (max_horizontal_pins + 1) as f64 * pin_spacing_mils
1165 } else {
1166 0.0
1167 };
1168 let width_mils = width_mils.max(min_width_for_tb);
1169
1170 let component = SchComponent {
1172 lib_reference: name.to_string(),
1173 component_description: description.unwrap_or_default(),
1174 part_count: 1,
1175 display_mode_count: 1,
1176 current_part_id: 1,
1177 ..Default::default()
1178 };
1179
1180 let mut primitives = vec![SchRecord::Component(component.clone())];
1181
1182 let mut rect_graphical = SchGraphicalBase::default();
1184 rect_graphical.base.owner_part_id = Some(1);
1185 rect_graphical.location_x = mils_to_raw(0);
1186 rect_graphical.location_y = mils_to_raw(0);
1187 rect_graphical.color = parse_color("800000")?; rect_graphical.area_color = parse_color("FFFFB0")?; let rect = SchRectangle {
1191 graphical: rect_graphical,
1192 corner_x: mils_f64_to_raw(width_mils),
1193 corner_y: mils_f64_to_raw(body_height_mils),
1194 line_width: LineWidth::Small,
1195 is_solid: true,
1196 transparent: false,
1197 ..Default::default()
1198 };
1199 primitives.push(SchRecord::Rectangle(rect));
1200
1201 for (i, pin_def) in left_pins.iter().enumerate() {
1203 let y_mils = body_height_mils - (i + 1) as f64 * pin_spacing_mils;
1204
1205 let mut graphical = SchGraphicalBase::default();
1206 graphical.base.owner_part_id = Some(1);
1207 graphical.location_x = mils_f64_to_raw(-pin_length_mils);
1208 graphical.location_y = mils_f64_to_raw(y_mils);
1209 graphical.color = 0x000080;
1210
1211 let pin = SchPin {
1212 graphical,
1213 designator: pin_def.designator.clone(),
1214 name: pin_def.name.clone(),
1215 electrical: pin_def.electrical,
1216 pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
1217 | PinConglomerateFlags::DESIGNATOR_VISIBLE,
1218 pin_length: mils_f64_to_raw(pin_length_mils),
1219 ..Default::default()
1220 };
1221 primitives.push(SchRecord::Pin(pin));
1222 }
1223
1224 for (i, pin_def) in right_pins.iter().enumerate() {
1226 let y_mils = body_height_mils - (i + 1) as f64 * pin_spacing_mils;
1227
1228 let mut graphical = SchGraphicalBase::default();
1229 graphical.base.owner_part_id = Some(1);
1230 graphical.location_x = mils_f64_to_raw(width_mils + pin_length_mils);
1231 graphical.location_y = mils_f64_to_raw(y_mils);
1232 graphical.color = 0x000080;
1233
1234 let pin = SchPin {
1235 graphical,
1236 designator: pin_def.designator.clone(),
1237 name: pin_def.name.clone(),
1238 electrical: pin_def.electrical,
1239 pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
1240 | PinConglomerateFlags::DESIGNATOR_VISIBLE
1241 | PinConglomerateFlags::FLIPPED,
1242 pin_length: mils_f64_to_raw(pin_length_mils),
1243 ..Default::default()
1244 };
1245 primitives.push(SchRecord::Pin(pin));
1246 }
1247
1248 for (i, pin_def) in top_pins.iter().enumerate() {
1250 let x_mils = (i + 1) as f64 * pin_spacing_mils;
1251
1252 let mut graphical = SchGraphicalBase::default();
1253 graphical.base.owner_part_id = Some(1);
1254 graphical.location_x = mils_f64_to_raw(x_mils);
1255 graphical.location_y = mils_f64_to_raw(body_height_mils + pin_length_mils);
1256 graphical.color = 0x000080;
1257
1258 let pin = SchPin {
1259 graphical,
1260 designator: pin_def.designator.clone(),
1261 name: pin_def.name.clone(),
1262 electrical: pin_def.electrical,
1263 pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
1264 | PinConglomerateFlags::DESIGNATOR_VISIBLE
1265 | PinConglomerateFlags::ROTATED,
1266 pin_length: mils_f64_to_raw(pin_length_mils),
1267 ..Default::default()
1268 };
1269 primitives.push(SchRecord::Pin(pin));
1270 }
1271
1272 for (i, pin_def) in bottom_pins.iter().enumerate() {
1274 let x_mils = (i + 1) as f64 * pin_spacing_mils;
1275
1276 let mut graphical = SchGraphicalBase::default();
1277 graphical.base.owner_part_id = Some(1);
1278 graphical.location_x = mils_f64_to_raw(x_mils);
1279 graphical.location_y = mils_f64_to_raw(-pin_length_mils);
1280 graphical.color = 0x000080;
1281
1282 let pin = SchPin {
1283 graphical,
1284 designator: pin_def.designator.clone(),
1285 name: pin_def.name.clone(),
1286 electrical: pin_def.electrical,
1287 pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
1288 | PinConglomerateFlags::DESIGNATOR_VISIBLE
1289 | PinConglomerateFlags::ROTATED
1290 | PinConglomerateFlags::FLIPPED,
1291 pin_length: mils_f64_to_raw(pin_length_mils),
1292 ..Default::default()
1293 };
1294 primitives.push(SchRecord::Pin(pin));
1295 }
1296
1297 let lib_component = SchLibComponent {
1298 component,
1299 primitives,
1300 };
1301
1302 lib.components.push(lib_component);
1303 save_schlib(path, &lib)?;
1304
1305 Ok(format!(
1306 "Generated IC symbol '{}' with {} pins ({} left, {} right, {} top, {} bottom)",
1307 name,
1308 pin_defs.len(),
1309 left_count,
1310 right_count,
1311 top_count,
1312 bottom_count,
1313 ))
1314}
1315
1316pub fn cmd_render_ascii(
1318 path: &Path,
1319 component_name: &str,
1320 max_width: usize,
1321 max_height: usize,
1322) -> Result<String, Box<dyn std::error::Error>> {
1323 let lib = open_schlib(path)?;
1324
1325 let name_lower = component_name.to_lowercase();
1326 let component = lib
1327 .components
1328 .iter()
1329 .find(|c| c.component.lib_reference.to_lowercase() == name_lower)
1330 .ok_or_else(|| format!("Component '{}' not found", component_name))?;
1331
1332 let mut min_x = i32::MAX;
1334 let mut min_y = i32::MAX;
1335 let mut max_x = i32::MIN;
1336 let mut max_y = i32::MIN;
1337
1338 for prim in &component.primitives {
1339 match prim {
1340 SchRecord::Pin(p) => {
1341 let (cx, cy) = p.get_corner();
1342 min_x = min_x.min(p.graphical.location_x).min(cx);
1343 min_y = min_y.min(p.graphical.location_y).min(cy);
1344 max_x = max_x.max(p.graphical.location_x).max(cx);
1345 max_y = max_y.max(p.graphical.location_y).max(cy);
1346 }
1347 SchRecord::Rectangle(r) => {
1348 min_x = min_x.min(r.graphical.location_x).min(r.corner_x);
1349 min_y = min_y.min(r.graphical.location_y).min(r.corner_y);
1350 max_x = max_x.max(r.graphical.location_x).max(r.corner_x);
1351 max_y = max_y.max(r.graphical.location_y).max(r.corner_y);
1352 }
1353 SchRecord::Line(l) => {
1354 min_x = min_x.min(l.graphical.location_x).min(l.corner_x);
1355 min_y = min_y.min(l.graphical.location_y).min(l.corner_y);
1356 max_x = max_x.max(l.graphical.location_x).max(l.corner_x);
1357 max_y = max_y.max(l.graphical.location_y).max(l.corner_y);
1358 }
1359 _ => {}
1360 }
1361 }
1362
1363 if min_x == i32::MAX {
1364 return Ok("No renderable primitives found.".to_string());
1365 }
1366
1367 let width_raw = (max_x - min_x) as f64;
1368 let height_raw = (max_y - min_y) as f64;
1369
1370 let scale_x = (max_width as f64 - 2.0) / width_raw;
1372 let scale_y = (max_height as f64 - 2.0) / height_raw;
1373 let scale = scale_x.min(scale_y);
1374
1375 let canvas_width = ((width_raw * scale) as usize + 2).min(max_width);
1376 let canvas_height = ((height_raw * scale) as usize + 2).min(max_height);
1377
1378 let mut canvas: Vec<Vec<char>> = vec![vec![' '; canvas_width]; canvas_height];
1380
1381 let to_canvas = |x: i32, y: i32| -> (usize, usize) {
1383 let cx = ((x - min_x) as f64 * scale) as usize;
1384 let cy = canvas_height - 1 - (((y - min_y) as f64 * scale) as usize);
1385 (cx.min(canvas_width - 1), cy.min(canvas_height - 1))
1386 };
1387
1388 for prim in &component.primitives {
1390 if let SchRecord::Rectangle(r) = prim {
1391 let (x1, y1) = to_canvas(r.graphical.location_x, r.graphical.location_y);
1392 let (x2, y2) = to_canvas(r.corner_x, r.corner_y);
1393 let (x1, x2) = (x1.min(x2), x1.max(x2));
1394 let (y1, y2) = (y1.min(y2), y1.max(y2));
1395
1396 for x in x1..=x2 {
1398 if y1 < canvas_height {
1399 canvas[y1][x.min(canvas_width - 1)] = '-';
1400 }
1401 if y2 < canvas_height {
1402 canvas[y2][x.min(canvas_width - 1)] = '-';
1403 }
1404 }
1405 for y in y1..=y2 {
1406 if x1 < canvas_width {
1407 canvas[y.min(canvas_height - 1)][x1] = '|';
1408 }
1409 if x2 < canvas_width {
1410 canvas[y.min(canvas_height - 1)][x2] = '|';
1411 }
1412 }
1413 if y1 < canvas_height && x1 < canvas_width {
1415 canvas[y1][x1] = '+';
1416 }
1417 if y1 < canvas_height && x2 < canvas_width {
1418 canvas[y1][x2] = '+';
1419 }
1420 if y2 < canvas_height && x1 < canvas_width {
1421 canvas[y2][x1] = '+';
1422 }
1423 if y2 < canvas_height && x2 < canvas_width {
1424 canvas[y2][x2] = '+';
1425 }
1426 }
1427 }
1428
1429 for prim in &component.primitives {
1431 if let SchRecord::Pin(p) = prim {
1432 let (px, py) = to_canvas(p.graphical.location_x, p.graphical.location_y);
1433 let (cx, cy) = p.get_corner();
1434 let (ex, ey) = to_canvas(cx, cy);
1435
1436 if px == ex {
1438 let y_start = py.min(ey);
1439 let y_end = py.max(ey);
1440 for row in canvas.iter_mut().take(y_end + 1).skip(y_start) {
1441 if let Some(cell) = row.get_mut(px) {
1442 *cell = '|';
1443 }
1444 }
1445 } else if let Some(row) = canvas.get_mut(py) {
1446 let x_start = px.min(ex);
1447 let x_end = px.max(ex);
1448 for cell in row.iter_mut().take(x_end + 1).skip(x_start) {
1449 *cell = '-';
1450 }
1451 }
1452
1453 if py < canvas_height && px < canvas_width {
1455 canvas[py][px] = 'o';
1456 }
1457 }
1458 }
1459
1460 let mut output = String::new();
1462 output.push_str(&format!("\n{}\n", component.component.lib_reference));
1463 output.push_str(&format!(
1464 "{}\n",
1465 "=".repeat(component.component.lib_reference.len())
1466 ));
1467 for row in &canvas {
1468 output.push_str(&format!("{}\n", row.iter().collect::<String>()));
1469 }
1470
1471 output.push_str("\nPins:\n");
1473 let mut pins: Vec<&SchPin> = component
1474 .primitives
1475 .iter()
1476 .filter_map(|p| {
1477 if let SchRecord::Pin(pin) = p {
1478 Some(pin)
1479 } else {
1480 None
1481 }
1482 })
1483 .collect();
1484 pins.sort_by(|a, b| alphanumeric_sort(&a.designator, &b.designator));
1485
1486 for pin in pins {
1487 output.push_str(&format!(
1488 " {} - {} ({})\n",
1489 pin.designator,
1490 pin.name,
1491 electrical_type_name(&pin.electrical)
1492 ));
1493 }
1494
1495 Ok(output)
1496}
1497
1498#[derive(Debug, Clone, Deserialize, Serialize)]
1505pub struct SchPinJson {
1506 pub designator: String,
1508 pub name: String,
1510 pub x: CoordValue,
1512 pub y: CoordValue,
1514 #[serde(default = "default_pin_length")]
1516 pub length: CoordValue,
1517 #[serde(default = "default_electrical")]
1519 pub electrical: String,
1520 #[serde(default = "default_orientation")]
1522 pub orientation: String,
1523 #[serde(default)]
1525 pub hidden: bool,
1526 #[serde(default)]
1528 pub description: String,
1529}
1530
1531fn default_pin_length() -> CoordValue {
1532 CoordValue(200.0)
1533}
1534
1535fn default_electrical() -> String {
1536 "passive".to_string()
1537}
1538
1539fn default_orientation() -> String {
1540 "right".to_string()
1541}
1542
1543#[derive(Debug, Clone, Deserialize, Serialize)]
1546pub struct SchRectangleJson {
1547 pub x1: CoordValue,
1549 pub y1: CoordValue,
1551 pub x2: CoordValue,
1553 pub y2: CoordValue,
1555 #[serde(default)]
1557 pub filled: bool,
1558 #[serde(default = "default_fill_color")]
1560 pub fill_color: String,
1561 #[serde(default = "default_border_color")]
1563 pub border_color: String,
1564}
1565
1566fn default_fill_color() -> String {
1567 "FFFFB0".to_string()
1568}
1569
1570fn default_border_color() -> String {
1571 "000080".to_string()
1572}
1573
1574#[derive(Debug, Clone, Deserialize, Serialize)]
1577pub struct SchLineJson {
1578 pub x1: CoordValue,
1580 pub y1: CoordValue,
1582 pub x2: CoordValue,
1584 pub y2: CoordValue,
1586 #[serde(default = "default_border_color")]
1588 pub color: String,
1589}
1590
1591#[derive(Debug, Clone, Deserialize, Serialize)]
1594pub struct SchPolygonJson {
1595 pub vertices: Vec<[CoordValue; 2]>,
1597 #[serde(default)]
1599 pub filled: bool,
1600 #[serde(default = "default_fill_color")]
1602 pub fill_color: String,
1603 #[serde(default = "default_border_color")]
1605 pub border_color: String,
1606}
1607
1608#[derive(Debug, Clone, Deserialize, Serialize)]
1611pub struct SchLabelJson {
1612 pub x: CoordValue,
1614 pub y: CoordValue,
1616 pub text: String,
1618 #[serde(default = "default_label_orientation")]
1620 pub orientation: String,
1621 #[serde(default = "default_justification")]
1624 pub justification: String,
1625 #[serde(default = "default_border_color")]
1627 pub color: String,
1628 #[serde(default = "default_font_id")]
1630 pub font_id: i32,
1631 #[serde(default)]
1633 pub hidden: bool,
1634}
1635
1636fn default_label_orientation() -> String {
1637 "horizontal".to_string()
1638}
1639
1640fn default_justification() -> String {
1641 "bottom_left".to_string()
1642}
1643
1644fn default_font_id() -> i32 {
1645 1
1646}
1647
1648#[derive(Debug, Clone, Deserialize, Serialize)]
1651pub struct SchArcJson {
1652 pub x: CoordValue,
1654 pub y: CoordValue,
1656 pub radius: CoordValue,
1658 #[serde(default)]
1660 pub start_angle: f64,
1661 #[serde(default = "default_end_angle")]
1663 pub end_angle: f64,
1664 #[serde(default = "default_border_color")]
1666 pub color: String,
1667}
1668
1669fn default_end_angle() -> f64 {
1670 360.0
1671}
1672
1673#[derive(Debug, Clone, Deserialize, Serialize)]
1676pub struct SchPolylineJson {
1677 pub vertices: Vec<[CoordValue; 2]>,
1679 #[serde(default = "default_border_color")]
1681 pub color: String,
1682}
1683
1684#[derive(Debug, Clone, Deserialize, Serialize)]
1687pub struct SchEllipseJson {
1688 pub x: CoordValue,
1690 pub y: CoordValue,
1692 pub radius_x: CoordValue,
1694 pub radius_y: CoordValue,
1696 #[serde(default)]
1698 pub filled: bool,
1699 #[serde(default = "default_fill_color")]
1701 pub fill_color: String,
1702 #[serde(default = "default_border_color")]
1704 pub border_color: String,
1705}
1706
1707#[derive(Debug, Clone, Deserialize, Serialize)]
1710pub struct SchComponentJson {
1711 pub name: String,
1713 #[serde(default)]
1715 pub description: String,
1716 #[serde(default = "default_part_count")]
1718 pub part_count: i32,
1719 #[serde(default)]
1721 pub pins: Vec<SchPinJson>,
1722 #[serde(default)]
1724 pub rectangles: Vec<SchRectangleJson>,
1725 #[serde(default)]
1727 pub lines: Vec<SchLineJson>,
1728 #[serde(default)]
1730 pub polygons: Vec<SchPolygonJson>,
1731 #[serde(default)]
1733 pub labels: Vec<SchLabelJson>,
1734 #[serde(default)]
1736 pub arcs: Vec<SchArcJson>,
1737 #[serde(default)]
1739 pub polylines: Vec<SchPolylineJson>,
1740 #[serde(default)]
1742 pub ellipses: Vec<SchEllipseJson>,
1743}
1744
1745fn default_part_count() -> i32 {
1746 1
1747}
1748
1749pub fn cmd_add_json(
1751 path: &Path,
1752 json_file: Option<String>,
1753 json_str: Option<String>,
1754) -> Result<String, Box<dyn std::error::Error>> {
1755 use std::io::{self, Read as IoRead};
1756
1757 let json_content = match (json_file, json_str) {
1759 (_, Some(s)) => s,
1760 (Some(ref path), None) if path == "-" => {
1761 let mut buffer = String::new();
1762 io::stdin().read_to_string(&mut buffer)?;
1763 buffer
1764 }
1765 (Some(ref file_path), None) => std::fs::read_to_string(file_path)?,
1766 (None, None) => {
1767 return Err("Must provide either --file <file> or --json <string>".into());
1768 }
1769 };
1770
1771 let component_def: SchComponentJson = serde_json::from_str(&json_content)?;
1773
1774 let mut lib = open_or_create_schlib(path)?;
1776
1777 if lib
1779 .components
1780 .iter()
1781 .any(|c| c.component.lib_reference == component_def.name)
1782 {
1783 return Err(format!("Component '{}' already exists", component_def.name).into());
1784 }
1785
1786 let component = SchComponent {
1788 lib_reference: component_def.name.clone(),
1789 component_description: component_def.description.clone(),
1790 part_count: component_def.part_count,
1791 display_mode_count: 1,
1792 current_part_id: 1,
1793 ..Default::default()
1794 };
1795
1796 let mut primitives = vec![SchRecord::Component(component.clone())];
1798
1799 for rect in &component_def.rectangles {
1801 let fill_color_val = parse_color(&rect.fill_color)?;
1802 let border_color_val = parse_color(&rect.border_color)?;
1803
1804 let mut graphical = SchGraphicalBase::default();
1805 graphical.base.owner_part_id = Some(1);
1806 graphical.location_x = rect.x1.to_raw();
1807 graphical.location_y = rect.y1.to_raw();
1808 graphical.color = border_color_val;
1809 graphical.area_color = fill_color_val;
1810
1811 let sch_rect = SchRectangle {
1812 graphical,
1813 corner_x: rect.x2.to_raw(),
1814 corner_y: rect.y2.to_raw(),
1815 line_width: LineWidth::Small,
1816 is_solid: rect.filled,
1817 transparent: !rect.filled,
1818 ..Default::default()
1819 };
1820 primitives.push(SchRecord::Rectangle(sch_rect));
1821 }
1822
1823 for line in &component_def.lines {
1825 let color_val = parse_color(&line.color)?;
1826
1827 let mut graphical = SchGraphicalBase::default();
1828 graphical.base.owner_part_id = Some(1);
1829 graphical.location_x = line.x1.to_raw();
1830 graphical.location_y = line.y1.to_raw();
1831 graphical.color = color_val;
1832
1833 let sch_line = SchLine {
1834 graphical,
1835 corner_x: line.x2.to_raw(),
1836 corner_y: line.y2.to_raw(),
1837 line_width: LineWidth::Small,
1838 ..Default::default()
1839 };
1840 primitives.push(SchRecord::Line(sch_line));
1841 }
1842
1843 for polygon in &component_def.polygons {
1845 if polygon.vertices.len() < 3 {
1846 return Err("Polygon must have at least 3 vertices".into());
1847 }
1848
1849 let fill_color_val = parse_color(&polygon.fill_color)?;
1850 let border_color_val = parse_color(&polygon.border_color)?;
1851
1852 let vertices: Vec<(i32, i32)> = polygon
1853 .vertices
1854 .iter()
1855 .map(|v| (v[0].to_raw(), v[1].to_raw()))
1856 .collect();
1857
1858 let mut graphical = SchGraphicalBase::default();
1859 graphical.base.owner_part_id = Some(1);
1860 graphical.location_x = vertices[0].0;
1861 graphical.location_y = vertices[0].1;
1862 graphical.color = border_color_val;
1863 graphical.area_color = fill_color_val;
1864
1865 let sch_polygon = SchPolygon {
1866 graphical,
1867 vertices,
1868 line_width: LineWidth::Small,
1869 is_solid: polygon.filled,
1870 transparent: !polygon.filled,
1871 ..Default::default()
1872 };
1873 primitives.push(SchRecord::Polygon(sch_polygon));
1874 }
1875
1876 for label in &component_def.labels {
1878 let color_val = parse_color(&label.color)?;
1879 let orientation = parse_text_orientation(&label.orientation)?;
1880 let justification = parse_text_justification(&label.justification)?;
1881
1882 let mut graphical = SchGraphicalBase::default();
1883 graphical.base.owner_part_id = Some(1);
1884 graphical.location_x = label.x.to_raw();
1885 graphical.location_y = label.y.to_raw();
1886 graphical.color = color_val;
1887
1888 let sch_label = SchLabel {
1889 graphical,
1890 text: label.text.clone(),
1891 orientation,
1892 justification,
1893 font_id: label.font_id,
1894 is_hidden: label.hidden,
1895 ..Default::default()
1896 };
1897 primitives.push(SchRecord::Label(sch_label));
1898 }
1899
1900 for arc in &component_def.arcs {
1902 let color_val = parse_color(&arc.color)?;
1903
1904 let mut graphical = SchGraphicalBase::default();
1905 graphical.base.owner_part_id = Some(1);
1906 graphical.location_x = arc.x.to_raw();
1907 graphical.location_y = arc.y.to_raw();
1908 graphical.color = color_val;
1909
1910 let sch_arc = SchArc {
1911 graphical,
1912 radius: arc.radius.to_raw(),
1913 secondary_radius: arc.radius.to_raw(), start_angle: arc.start_angle,
1915 end_angle: arc.end_angle,
1916 line_width: LineWidth::Small,
1917 ..Default::default()
1918 };
1919 primitives.push(SchRecord::Arc(sch_arc));
1920 }
1921
1922 for polyline in &component_def.polylines {
1924 if polyline.vertices.len() < 2 {
1925 return Err("Polyline must have at least 2 vertices".into());
1926 }
1927
1928 let color_val = parse_color(&polyline.color)?;
1929
1930 let vertices: Vec<(i32, i32)> = polyline
1931 .vertices
1932 .iter()
1933 .map(|v| (v[0].to_raw(), v[1].to_raw()))
1934 .collect();
1935
1936 let mut graphical = SchGraphicalBase::default();
1937 graphical.base.owner_part_id = Some(1);
1938 graphical.location_x = vertices[0].0;
1939 graphical.location_y = vertices[0].1;
1940 graphical.color = color_val;
1941
1942 let sch_polyline = SchPolyline {
1943 graphical,
1944 vertices,
1945 line_width: LineWidth::Small,
1946 ..Default::default()
1947 };
1948 primitives.push(SchRecord::Polyline(sch_polyline));
1949 }
1950
1951 for ellipse in &component_def.ellipses {
1953 let fill_color_val = parse_color(&ellipse.fill_color)?;
1954 let border_color_val = parse_color(&ellipse.border_color)?;
1955
1956 let mut graphical = SchGraphicalBase::default();
1957 graphical.base.owner_part_id = Some(1);
1958 graphical.location_x = ellipse.x.to_raw();
1959 graphical.location_y = ellipse.y.to_raw();
1960 graphical.color = border_color_val;
1961 graphical.area_color = fill_color_val;
1962
1963 let sch_ellipse = SchEllipse {
1964 graphical,
1965 radius_x: ellipse.radius_x.to_raw(),
1966 radius_y: ellipse.radius_y.to_raw(),
1967 is_solid: ellipse.filled,
1968 transparent: !ellipse.filled,
1969 line_width: LineWidth::Small,
1970 ..Default::default()
1971 };
1972 primitives.push(SchRecord::Ellipse(sch_ellipse));
1973 }
1974
1975 for pin_def in &component_def.pins {
1977 let electrical_type = parse_electrical_type(&pin_def.electrical)?;
1978 let mut conglomerate = parse_pin_orientation(&pin_def.orientation)?;
1979
1980 conglomerate |= PinConglomerateFlags::DISPLAY_NAME_VISIBLE;
1981 conglomerate |= PinConglomerateFlags::DESIGNATOR_VISIBLE;
1982
1983 if pin_def.hidden {
1984 conglomerate |= PinConglomerateFlags::HIDE;
1985 }
1986
1987 let mut graphical = SchGraphicalBase::default();
1988 graphical.base.owner_part_id = Some(1);
1989 graphical.location_x = pin_def.x.to_raw();
1990 graphical.location_y = pin_def.y.to_raw();
1991 graphical.color = 0x000080;
1992
1993 let pin = SchPin {
1994 graphical,
1995 designator: pin_def.designator.clone(),
1996 name: pin_def.name.clone(),
1997 electrical: electrical_type,
1998 pin_conglomerate: conglomerate,
1999 pin_length: pin_def.length.to_raw(),
2000 description: pin_def.description.clone(),
2001 symbol_inner_edge: PinSymbol::None,
2002 symbol_outer_edge: PinSymbol::None,
2003 symbol_inside: PinSymbol::None,
2004 symbol_outside: PinSymbol::None,
2005 ..Default::default()
2006 };
2007 primitives.push(SchRecord::Pin(pin));
2008 }
2009
2010 let pin_count = component_def.pins.len();
2011 let rect_count = component_def.rectangles.len();
2012 let line_count = component_def.lines.len();
2013 let polygon_count = component_def.polygons.len();
2014 let label_count = component_def.labels.len();
2015 let arc_count = component_def.arcs.len();
2016 let polyline_count = component_def.polylines.len();
2017 let ellipse_count = component_def.ellipses.len();
2018
2019 let lib_component = SchLibComponent {
2020 component,
2021 primitives,
2022 };
2023
2024 lib.components.push(lib_component);
2025 save_schlib(path, &lib)?;
2026
2027 let mut parts = vec![format!("{} pins", pin_count)];
2029 if rect_count > 0 {
2030 parts.push(format!("{} rectangles", rect_count));
2031 }
2032 if line_count > 0 {
2033 parts.push(format!("{} lines", line_count));
2034 }
2035 if polygon_count > 0 {
2036 parts.push(format!("{} polygons", polygon_count));
2037 }
2038 if label_count > 0 {
2039 parts.push(format!("{} labels", label_count));
2040 }
2041 if arc_count > 0 {
2042 parts.push(format!("{} arcs", arc_count));
2043 }
2044 if polyline_count > 0 {
2045 parts.push(format!("{} polylines", polyline_count));
2046 }
2047 if ellipse_count > 0 {
2048 parts.push(format!("{} ellipses", ellipse_count));
2049 }
2050
2051 Ok(format!(
2052 "Added component '{}' with {} to {}",
2053 component_def.name,
2054 parts.join(", "),
2055 path.display()
2056 ))
2057}
2058
2059fn parse_text_orientation(s: &str) -> Result<TextOrientations, Box<dyn std::error::Error>> {
2061 match s.to_lowercase().as_str() {
2062 "horizontal" | "0" => Ok(TextOrientations::NONE),
2063 "vertical_up" | "90" | "up" => Ok(TextOrientations::ROTATED),
2064 "vertical_down" | "270" | "down" => Ok(TextOrientations::ROTATED | TextOrientations::FLIPPED),
2065 "180" | "flipped" => Ok(TextOrientations::FLIPPED),
2066 _ => Err(format!("Unknown text orientation: {}. Use: horizontal, vertical_up, vertical_down, 90, 180, 270", s).into()),
2067 }
2068}
2069
2070fn parse_text_justification(s: &str) -> Result<TextJustification, Box<dyn std::error::Error>> {
2072 match s.to_lowercase().replace('_', "").as_str() {
2073 "bottomleft" | "bl" => Ok(TextJustification::BOTTOM_LEFT),
2074 "bottomcenter" | "bc" => Ok(TextJustification::BOTTOM_CENTER),
2075 "bottomright" | "br" => Ok(TextJustification::BOTTOM_RIGHT),
2076 "centerleft" | "cl" | "middleleft" | "ml" => Ok(TextJustification::MIDDLE_LEFT),
2077 "center" | "c" | "middle" | "m" => Ok(TextJustification::MIDDLE_CENTER),
2078 "centerright" | "cr" | "middleright" | "mr" => Ok(TextJustification::MIDDLE_RIGHT),
2079 "topleft" | "tl" => Ok(TextJustification::TOP_LEFT),
2080 "topcenter" | "tc" => Ok(TextJustification::TOP_CENTER),
2081 "topright" | "tr" => Ok(TextJustification::TOP_RIGHT),
2082 _ => Err(format!("Unknown justification: {}. Use: bottom_left, bottom_center, bottom_right, center_left, center, center_right, top_left, top_center, top_right", s).into()),
2083 }
2084}
2085
2086#[cfg(test)]
2087mod tests {
2088 use super::*;
2089 use std::path::PathBuf;
2090
2091 fn temp_schlib() -> PathBuf {
2092 let id = uuid::Uuid::new_v4();
2093 std::env::temp_dir().join(format!("test_{}.SchLib", id))
2094 }
2095
2096 #[test]
2099 fn test_gen_ic_all_four_sides() {
2100 let path = temp_schlib();
2101 cmd_create(&path).unwrap();
2102
2103 let result = cmd_gen_ic(
2104 &path,
2105 "TEST_4SIDE",
2106 "1:VCC:power:top,2:GND:power:bottom,3:IN:input:left,4:OUT:output:right",
2107 Some("4-side test".to_string()),
2108 "600mil",
2109 "200mil",
2110 "100mil",
2111 )
2112 .unwrap();
2113
2114 assert!(result.contains("4 pins"), "Expected 4 pins, got: {}", result);
2115 assert!(result.contains("1 left"), "Expected 1 left pin: {}", result);
2116 assert!(result.contains("1 right"), "Expected 1 right pin: {}", result);
2117 assert!(result.contains("1 top"), "Expected 1 top pin: {}", result);
2118 assert!(result.contains("1 bottom"), "Expected 1 bottom pin: {}", result);
2119
2120 let lib = open_or_create_schlib(&path).unwrap();
2122 let comp = lib
2123 .components
2124 .iter()
2125 .find(|c| c.component.lib_reference == "TEST_4SIDE")
2126 .expect("Component must exist");
2127
2128 let pin_count = comp
2129 .primitives
2130 .iter()
2131 .filter(|r| matches!(r, SchRecord::Pin(_)))
2132 .count();
2133 assert_eq!(pin_count, 4, "All 4 pins must be saved, got {}", pin_count);
2134
2135 std::fs::remove_file(&path).ok();
2136 }
2137
2138 #[test]
2140 fn test_gen_ic_only_top_bottom() {
2141 let path = temp_schlib();
2142 cmd_create(&path).unwrap();
2143
2144 let result = cmd_gen_ic(
2145 &path,
2146 "PWR_2PIN",
2147 "1:VCC:power:top,2:GND:power:bottom",
2148 None,
2149 "400mil",
2150 "200mil",
2151 "100mil",
2152 )
2153 .unwrap();
2154
2155 assert!(result.contains("2 pins"), "Expected 2 pins, got: {}", result);
2156 assert!(result.contains("1 top"), "Expected 1 top: {}", result);
2157 assert!(result.contains("1 bottom"), "Expected 1 bottom: {}", result);
2158
2159 std::fs::remove_file(&path).ok();
2160 }
2161
2162 #[test]
2164 fn test_gen_ic_multi_pin_sides() {
2165 let path = temp_schlib();
2166 cmd_create(&path).unwrap();
2167
2168 let pins = [
2170 "1:A:io:left", "2:B:io:left", "3:C:io:left",
2171 "4:D:io:right", "5:E:io:right", "6:F:io:right", "7:G:io:right",
2172 "8:VCC:power:top", "9:VCC2:power:top",
2173 "10:GND:power:bottom", "11:GND2:power:bottom",
2174 ]
2175 .join(",");
2176
2177 let result = cmd_gen_ic(
2178 &path,
2179 "MULTI_PIN",
2180 &pins,
2181 None,
2182 "800mil",
2183 "200mil",
2184 "100mil",
2185 )
2186 .unwrap();
2187
2188 assert!(result.contains("11 pins"), "Expected 11 pins, got: {}", result);
2189 assert!(result.contains("3 left"), "Got: {}", result);
2190 assert!(result.contains("4 right"), "Got: {}", result);
2191 assert!(result.contains("2 top"), "Got: {}", result);
2192 assert!(result.contains("2 bottom"), "Got: {}", result);
2193
2194 let lib = open_or_create_schlib(&path).unwrap();
2196 let comp = lib
2197 .components
2198 .iter()
2199 .find(|c| c.component.lib_reference == "MULTI_PIN")
2200 .unwrap();
2201 let pin_count = comp
2202 .primitives
2203 .iter()
2204 .filter(|r| matches!(r, SchRecord::Pin(_)))
2205 .count();
2206 assert_eq!(pin_count, 11);
2207
2208 std::fs::remove_file(&path).ok();
2209 }
2210
2211 #[test]
2213 fn test_gen_ic_body_widens_for_top_bottom() {
2214 let path = temp_schlib();
2215 cmd_create(&path).unwrap();
2216
2217 let pins = "1:A:io:top,2:B:io:top,3:C:io:top,4:D:io:top,5:E:io:top,6:IN:input:left";
2220
2221 cmd_gen_ic(
2222 &path,
2223 "WIDE_TOP",
2224 pins,
2225 None,
2226 "400mil", "200mil",
2228 "100mil",
2229 )
2230 .unwrap();
2231
2232 let lib = open_or_create_schlib(&path).unwrap();
2233 let comp = lib
2234 .components
2235 .iter()
2236 .find(|c| c.component.lib_reference == "WIDE_TOP")
2237 .unwrap();
2238
2239 let rect = comp.primitives.iter().find_map(|r| {
2241 if let SchRecord::Rectangle(rect) = r {
2242 Some(rect)
2243 } else {
2244 None
2245 }
2246 }).expect("Must have body rectangle");
2247
2248 let body_width_mils = rect.corner_x as f64 / 10000.0;
2250 assert!(
2251 body_width_mils >= 600.0,
2252 "Body width should expand to at least 600mil for 5 top pins, got {}mil",
2253 body_width_mils
2254 );
2255
2256 std::fs::remove_file(&path).ok();
2257 }
2258
2259 #[test]
2261 fn test_gen_ic_invalid_pin_type() {
2262 let path = temp_schlib();
2263 cmd_create(&path).unwrap();
2264
2265 let result = cmd_gen_ic(
2266 &path,
2267 "BAD",
2268 "1:VCC:W:left", None,
2270 "400mil",
2271 "200mil",
2272 "100mil",
2273 );
2274
2275 assert!(result.is_err(), "Invalid pin type should fail");
2276 let err = result.unwrap_err().to_string();
2277 assert!(
2278 err.contains("Unknown electrical type"),
2279 "Error should mention electrical type, got: {}",
2280 err
2281 );
2282
2283 std::fs::remove_file(&path).ok();
2284 }
2285}