use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Cursor};
use std::path::Path;
use serde::{Deserialize, Serialize};
use serde_json;
use super::util::alphanumeric_sort;
use crate::dump::fmt_coord;
use crate::io::{SchLib, SchLibComponent};
use crate::ops::categorization::categorize_component;
use crate::ops::output::*;
use crate::records::sch::{
LineWidth, PinConglomerateFlags, PinElectricalType, PinSymbol, SchArc, SchComponent,
SchEllipse, SchGraphicalBase, SchLabel, SchLine, SchPin, SchPolygon, SchPolyline, SchRecord,
SchRectangle, TextJustification, TextOrientations,
};
use crate::types::Unit;
fn open_schlib(path: &Path) -> Result<SchLib, Box<dyn std::error::Error>> {
let file = File::open(path)?;
Ok(SchLib::open(BufReader::new(file))?)
}
fn electrical_type_name(et: &PinElectricalType) -> &'static str {
match et {
PinElectricalType::Input => "Input",
PinElectricalType::InputOutput => "I/O",
PinElectricalType::Output => "Output",
PinElectricalType::OpenCollector => "Open Collector",
PinElectricalType::Passive => "Passive",
PinElectricalType::HiZ => "Hi-Z",
PinElectricalType::OpenEmitter => "Open Emitter",
PinElectricalType::Power => "Power",
}
}
fn record_type_name(record: &SchRecord) -> &'static str {
match record {
SchRecord::Component(_) => "Component",
SchRecord::Pin(_) => "Pin",
SchRecord::Symbol(_) => "Symbol",
SchRecord::Label(_) => "Label",
SchRecord::Bezier(_) => "Bezier",
SchRecord::Polyline(_) => "Polyline",
SchRecord::Polygon(_) => "Polygon",
SchRecord::Ellipse(_) => "Ellipse",
SchRecord::Pie(_) => "Pie",
SchRecord::EllipticalArc(_) => "EllipticalArc",
SchRecord::Arc(_) => "Arc",
SchRecord::Line(_) => "Line",
SchRecord::Rectangle(_) => "Rectangle",
SchRecord::PowerObject(_) => "PowerObject",
SchRecord::Port(_) => "Port",
SchRecord::NoErc(_) => "NoERC",
SchRecord::NetLabel(_) => "NetLabel",
SchRecord::Bus(_) => "Bus",
SchRecord::Wire(_) => "Wire",
SchRecord::TextFrame(_) => "TextFrame",
SchRecord::TextFrameVariant(_) => "TextFrameVariant",
SchRecord::Junction(_) => "Junction",
SchRecord::Image(_) => "Image",
SchRecord::SheetHeader(_) => "SheetHeader",
SchRecord::Designator(_) => "Designator",
SchRecord::BusEntry(_) => "BusEntry",
SchRecord::Parameter(_) => "Parameter",
SchRecord::WarningSign(_) => "WarningSign",
SchRecord::ImplementationList(_) => "ImplementationList",
SchRecord::Implementation(_) => "Implementation",
SchRecord::MapDefinerList(_) => "MapDefinerList",
SchRecord::MapDefiner(_) => "MapDefiner",
SchRecord::ImplementationParameters(_) => "ImplementationParameters",
SchRecord::Unknown { .. } => "Unknown",
}
}
pub fn cmd_overview(path: &Path, full: bool) -> Result<SchLibOverview, Box<dyn std::error::Error>> {
let lib = open_schlib(path)?;
let mut categories: HashMap<&'static str, Vec<ComponentSummary>> = HashMap::new();
for comp in lib.iter() {
let category = categorize_component(
&comp.component.lib_reference,
&comp.component.component_description,
);
categories
.entry(category)
.or_default()
.push(ComponentSummary {
name: comp.component.lib_reference.clone(),
description: comp.component.component_description.clone(),
pin_count: comp.pin_count(),
part_count: comp.component.part_count,
});
}
let category_order = [
"Microcontroller",
"FPGA/CPLD",
"Memory",
"ADC",
"DAC",
"Transceiver/PHY",
"Clock/Oscillator",
"Power Supply",
"Amplifier",
"Mux/Switch",
"Buffer/Driver",
"Other IC",
"Transistor",
"Diode/Protection",
"LED",
"Capacitor",
"Resistor",
"Inductor/Ferrite",
"Connector",
"Test Point",
];
let mut components_by_category = Vec::new();
for category in category_order.iter() {
if let Some(comps) = categories.remove(*category) {
components_by_category.push((category.to_string(), comps));
}
}
for (category, comps) in categories {
if !comps.is_empty() {
components_by_category.push((category.to_string(), comps));
}
}
let mut total_pins = 0;
let mut pin_types: HashMap<String, usize> = HashMap::new();
for comp in lib.iter() {
for prim in &comp.primitives {
if let SchRecord::Pin(pin) = prim {
total_pins += 1;
*pin_types
.entry(electrical_type_name(&pin.electrical).to_string())
.or_insert(0) += 1;
}
}
}
let mut sorted_types: Vec<_> = pin_types.into_iter().collect();
sorted_types.sort_by(|a, b| b.1.cmp(&a.1));
let pin_statistics = PinStatistics {
total_pins,
pin_types: sorted_types,
};
let multi_part_components: Vec<ComponentSummary> = lib
.iter()
.filter(|c| c.component.part_count > 1)
.map(|comp| ComponentSummary {
name: comp.component.lib_reference.clone(),
description: comp.component.component_description.clone(),
pin_count: comp.pin_count(),
part_count: comp.component.part_count,
})
.collect();
let mut largest_components: Vec<ComponentSummary> = lib
.iter()
.map(|comp| ComponentSummary {
name: comp.component.lib_reference.clone(),
description: comp.component.component_description.clone(),
pin_count: comp.pin_count(),
part_count: comp.component.part_count,
})
.collect();
largest_components.sort_by(|a, b| b.pin_count.cmp(&a.pin_count));
largest_components.truncate(10);
let component_details = if full {
Some(
lib.iter()
.map(|comp| {
let pins = comp
.primitives
.iter()
.filter_map(|prim| {
if let SchRecord::Pin(pin) = prim {
Some(PinDetail {
designator: pin.designator.clone(),
name: pin.name.clone(),
electrical_type: electrical_type_name(&pin.electrical)
.to_string(),
description: pin.description.clone(),
})
} else {
None
}
})
.collect();
SchLibComponentDetail {
name: comp.component.lib_reference.clone(),
description: comp.component.component_description.clone(),
part_count: comp.component.part_count,
display_mode_count: comp.component.display_mode_count,
pin_count: comp.pin_count(),
total_primitives: comp.primitives.len(),
pins,
primitive_counts: None,
}
})
.collect(),
)
} else {
None
};
Ok(SchLibOverview {
path: path.display().to_string(),
total_components: lib.components.len(),
components_by_category,
pin_statistics,
multi_part_components,
largest_components,
component_details,
})
}
pub fn cmd_list(path: &Path) -> Result<SchLibComponentList, Box<dyn std::error::Error>> {
let lib = open_schlib(path)?;
let components: Vec<ComponentSummary> = lib
.iter()
.map(|comp| ComponentSummary {
name: comp.component.lib_reference.clone(),
description: comp.component.component_description.clone(),
pin_count: comp.pin_count(),
part_count: comp.component.part_count,
})
.collect();
Ok(SchLibComponentList {
path: path.display().to_string(),
total_components: lib.components.len(),
components,
})
}
pub fn cmd_search(
path: &Path,
query: &str,
limit: Option<usize>,
) -> Result<SchLibSearchResults, Box<dyn std::error::Error>> {
let lib = open_schlib(path)?;
let query_lower = query.to_lowercase();
let has_wildcard = query.contains('*');
let matches: Vec<ComponentSummary> = lib
.iter()
.filter(|comp| {
let name = comp.component.lib_reference.to_lowercase();
let desc = comp.component.component_description.to_lowercase();
if has_wildcard {
let pattern = query_lower.replace('*', "");
name.contains(&pattern) || desc.contains(&pattern)
} else {
name.contains(&query_lower) || desc.contains(&query_lower)
}
})
.map(|comp| ComponentSummary {
name: comp.component.lib_reference.clone(),
description: comp.component.component_description.clone(),
pin_count: comp.pin_count(),
part_count: comp.component.part_count,
})
.collect();
let total_matches = matches.len();
let results = if let Some(limit) = limit {
matches.into_iter().take(limit).collect()
} else {
matches
};
Ok(SchLibSearchResults {
query: query.to_string(),
total_matches,
results,
})
}
pub fn cmd_info(path: &Path) -> Result<SchLibInfo, Box<dyn std::error::Error>> {
let lib = open_schlib(path)?;
let mut primitive_counts: HashMap<String, usize> = HashMap::new();
let mut total_primitives = 0;
for comp in lib.iter() {
for prim in &comp.primitives {
let name = record_type_name(prim).to_string();
*primitive_counts.entry(name).or_insert(0) += 1;
total_primitives += 1;
}
}
let mut sorted: Vec<_> = primitive_counts.into_iter().collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1));
let multi_part_count = lib.iter().filter(|c| c.component.part_count > 1).count();
Ok(SchLibInfo {
path: path.display().to_string(),
component_count: lib.components.len(),
total_primitives,
primitive_types: sorted,
multi_part_count,
})
}
pub fn cmd_component(
path: &Path,
name: &str,
show_primitives: bool,
) -> Result<SchLibComponentDetail, Box<dyn std::error::Error>> {
let lib = open_schlib(path)?;
let name_lower = name.to_lowercase();
let comp = lib
.iter()
.find(|c| c.component.lib_reference.to_lowercase() == name_lower)
.ok_or_else(|| format!("Component '{}' not found", name))?;
let mut pins: Vec<&SchPin> = comp
.primitives
.iter()
.filter_map(|p| {
if let SchRecord::Pin(pin) = p {
Some(pin)
} else {
None
}
})
.collect();
pins.sort_by(|a, b| alphanumeric_sort(&a.designator, &b.designator));
let pins_detail: Vec<PinDetail> = pins
.iter()
.map(|pin| PinDetail {
designator: pin.designator.clone(),
name: pin.name.clone(),
electrical_type: electrical_type_name(&pin.electrical).to_string(),
description: pin.description.clone(),
})
.collect();
let primitive_counts = if show_primitives {
let mut prim_counts: HashMap<String, usize> = HashMap::new();
for prim in &comp.primitives {
*prim_counts
.entry(record_type_name(prim).to_string())
.or_insert(0) += 1;
}
Some(prim_counts.into_iter().collect())
} else {
None
};
Ok(SchLibComponentDetail {
name: comp.component.lib_reference.clone(),
description: comp.component.component_description.clone(),
part_count: comp.component.part_count,
display_mode_count: comp.component.display_mode_count,
pin_count: comp.pin_count(),
total_primitives: comp.primitive_count(),
pins: pins_detail,
primitive_counts,
})
}
pub fn cmd_pins(
path: &Path,
component_filter: Option<String>,
by_type: bool,
) -> Result<SchLibPinList, Box<dyn std::error::Error>> {
let lib = open_schlib(path)?;
let filter_lower = component_filter.as_ref().map(|s| s.to_lowercase());
let mut all_pins: Vec<PinWithComponent> = Vec::new();
for comp in lib.iter() {
if let Some(ref filter) = filter_lower {
if !comp.component.lib_reference.to_lowercase().contains(filter) {
continue;
}
}
for prim in &comp.primitives {
if let SchRecord::Pin(pin) = prim {
all_pins.push(PinWithComponent {
component_name: comp.component.lib_reference.clone(),
designator: pin.designator.clone(),
name: pin.name.clone(),
electrical_type: electrical_type_name(&pin.electrical).to_string(),
});
}
}
}
let pins_by_type = if by_type {
let mut by_type: HashMap<String, Vec<PinWithComponent>> = HashMap::new();
for pin in &all_pins {
by_type
.entry(pin.electrical_type.clone())
.or_default()
.push(pin.clone());
}
let type_order = [
"Input",
"Output",
"I/O",
"Passive",
"Power",
"Open Collector",
"Open Emitter",
"Hi-Z",
];
let mut ordered: Vec<(String, Vec<PinWithComponent>)> = Vec::new();
for etype in type_order {
if let Some(pins) = by_type.remove(etype) {
ordered.push((etype.to_string(), pins));
}
}
for (etype, pins) in by_type {
ordered.push((etype, pins));
}
Some(ordered)
} else {
None
};
Ok(SchLibPinList {
path: path.display().to_string(),
total_pins: all_pins.len(),
pins: all_pins,
pins_by_type,
})
}
pub fn cmd_primitives(
path: &Path,
name: &str,
) -> Result<SchLibPrimitiveList, Box<dyn std::error::Error>> {
let lib = open_schlib(path)?;
let name_lower = name.to_lowercase();
let comp = lib
.iter()
.find(|c| c.component.lib_reference.to_lowercase() == name_lower)
.ok_or_else(|| format!("Component '{}' not found", name))?;
let primitives: Vec<PrimitiveInfo> = comp
.primitives
.iter()
.skip(1)
.map(|prim| match prim {
SchRecord::Pin(p) => PrimitiveInfo::Pin {
designator: p.designator.clone(),
name: p.name.clone(),
electrical_type: electrical_type_name(&p.electrical).to_string(),
x: fmt_coord(p.graphical.location_x),
y: fmt_coord(p.graphical.location_y),
},
SchRecord::Rectangle(r) => PrimitiveInfo::Rectangle {
x1: fmt_coord(r.graphical.location_x),
y1: fmt_coord(r.graphical.location_y),
x2: fmt_coord(r.corner_x),
y2: fmt_coord(r.corner_y),
},
SchRecord::Line(l) => PrimitiveInfo::Line {
x1: fmt_coord(l.graphical.location_x),
y1: fmt_coord(l.graphical.location_y),
x2: fmt_coord(l.corner_x),
y2: fmt_coord(l.corner_y),
},
SchRecord::Arc(a) => PrimitiveInfo::Arc {
center_x: fmt_coord(a.graphical.location_x),
center_y: fmt_coord(a.graphical.location_y),
radius: fmt_coord(a.radius),
start_angle: a.start_angle,
end_angle: a.end_angle,
},
SchRecord::Polygon(p) => PrimitiveInfo::Polygon {
vertex_count: p.vertices.len(),
},
SchRecord::Polyline(p) => PrimitiveInfo::Polyline {
vertex_count: p.vertices.len(),
},
SchRecord::Label(l) => PrimitiveInfo::Label {
text: l.text.clone(),
x: fmt_coord(l.graphical.location_x),
y: fmt_coord(l.graphical.location_y),
},
_ => PrimitiveInfo::Other {
primitive_type: record_type_name(prim).to_string(),
},
})
.collect();
Ok(SchLibPrimitiveList {
component_name: comp.component.lib_reference.clone(),
total_primitives: comp.primitive_count(),
primitives,
})
}
pub fn cmd_json(path: &Path) -> Result<SchLibComponentList, Box<dyn std::error::Error>> {
cmd_list(path)
}
const BLANK_SCHLIB_TEMPLATE: &[u8] = include_bytes!("../../data/blank/Schlib1.SchLib");
pub fn cmd_create(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
if path.exists() {
return Err(format!("File already exists: {}", path.display()).into());
}
std::fs::write(path, BLANK_SCHLIB_TEMPLATE)?;
Ok(format!("Created empty SchLib: {}", path.display()))
}
fn load_blank_schlib() -> Result<SchLib, Box<dyn std::error::Error>> {
Ok(SchLib::open(Cursor::new(BLANK_SCHLIB_TEMPLATE))?)
}
fn open_or_create_schlib(path: &Path) -> Result<SchLib, Box<dyn std::error::Error>> {
if path.exists() {
open_schlib(path)
} else {
load_blank_schlib()
}
}
fn save_schlib(path: &Path, lib: &SchLib) -> Result<(), Box<dyn std::error::Error>> {
Ok(lib.save_to_file(path)?)
}
fn parse_color(hex: &str) -> Result<i32, Box<dyn std::error::Error>> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Err(format!(
"Invalid color format: {}. Expected 6 hex digits (RRGGBB)",
hex
)
.into());
}
let r = u8::from_str_radix(&hex[0..2], 16)
.map_err(|_| format!("Invalid red component in color: {}", hex))?;
let g = u8::from_str_radix(&hex[2..4], 16)
.map_err(|_| format!("Invalid green component in color: {}", hex))?;
let b = u8::from_str_radix(&hex[4..6], 16)
.map_err(|_| format!("Invalid blue component in color: {}", hex))?;
Ok((b as i32) << 16 | (g as i32) << 8 | (r as i32))
}
fn parse_electrical_type(s: &str) -> Result<PinElectricalType, Box<dyn std::error::Error>> {
match s.to_lowercase().as_str() {
"input" | "in" => Ok(PinElectricalType::Input),
"output" | "out" => Ok(PinElectricalType::Output),
"io" | "inputoutput" | "bidirectional" | "bidir" => Ok(PinElectricalType::InputOutput),
"passive" | "pas" => Ok(PinElectricalType::Passive),
"power" | "pwr" => Ok(PinElectricalType::Power),
"oc" | "opencollector" => Ok(PinElectricalType::OpenCollector),
"oe" | "openemitter" => Ok(PinElectricalType::OpenEmitter),
"hiz" | "tristate" | "3state" => Ok(PinElectricalType::HiZ),
_ => Err(format!(
"Unknown electrical type: {}. Use: input, output, io, passive, power, oc, oe, hiz",
s
)
.into()),
}
}
fn parse_pin_orientation(s: &str) -> Result<PinConglomerateFlags, Box<dyn std::error::Error>> {
match s.to_lowercase().as_str() {
"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()),
}
}
fn mils_to_raw(mils: i32) -> i32 {
mils * 10000
}
fn mils_f64_to_raw(mils: f64) -> i32 {
(mils * 10000.0).round() as i32
}
#[allow(dead_code)] fn parse_unit_value_to_mils(s: &str) -> Result<f64, Box<dyn std::error::Error>> {
let (coord, _unit) =
Unit::parse_with_unit(s).map_err(|e| format!("Invalid value '{}': {:?}", s, e))?;
Ok(coord.to_mils())
}
fn parse_unit_value_or_mil(s: &str) -> Result<f64, Box<dyn std::error::Error>> {
let s = s.trim();
if let Ok((coord, _unit)) = Unit::parse_with_unit(s) {
return Ok(coord.to_mils());
}
s.parse::<f64>().map_err(|_| {
format!(
"Invalid value '{}': expected number with optional unit (e.g., '100mil', '2.54mm')",
s
)
.into()
})
}
#[derive(Debug, Clone)]
pub struct CoordValue(pub f64);
impl CoordValue {
pub fn to_mils(&self) -> f64 {
self.0
}
pub fn to_raw(&self) -> i32 {
mils_f64_to_raw(self.0)
}
}
impl Default for CoordValue {
fn default() -> Self {
CoordValue(0.0)
}
}
impl<'de> Deserialize<'de> for CoordValue {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct CoordValueVisitor;
impl<'de> Visitor<'de> for CoordValueVisitor {
type Value = CoordValue;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(
"a number (mils) or a string with unit (e.g., \"100mil\", \"2.54mm\")",
)
}
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(CoordValue(value as f64))
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(CoordValue(value as f64))
}
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(CoordValue(value))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
parse_unit_value_or_mil(value)
.map(CoordValue)
.map_err(de::Error::custom)
}
}
deserializer.deserialize_any(CoordValueVisitor)
}
}
impl Serialize for CoordValue {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_f64(self.0)
}
}
pub fn cmd_add_component(
path: &Path,
name: &str,
description: Option<String>,
) -> Result<String, Box<dyn std::error::Error>> {
let mut lib = open_or_create_schlib(path)?;
if lib
.components
.iter()
.any(|c| c.component.lib_reference == name)
{
return Err(format!("Component '{}' already exists", name).into());
}
let component = SchComponent {
lib_reference: name.to_string(),
component_description: description.unwrap_or_default(),
part_count: 1,
display_mode_count: 1,
current_part_id: 1,
..Default::default()
};
let lib_component = SchLibComponent {
component: component.clone(),
primitives: vec![SchRecord::Component(component)],
};
lib.components.push(lib_component);
save_schlib(path, &lib)?;
Ok(format!("Added component '{}' to {}", name, path.display()))
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_add_pin(
path: &Path,
component_name: &str,
designator: &str,
name: &str,
x: &str,
y: &str,
length: &str,
electrical: &str,
orientation: &str,
hidden: bool,
) -> Result<String, Box<dyn std::error::Error>> {
let mut lib = open_schlib(path)?;
let x_mils = parse_unit_value_or_mil(x)?;
let y_mils = parse_unit_value_or_mil(y)?;
let length_mils = parse_unit_value_or_mil(length)?;
let component = lib
.components
.iter_mut()
.find(|c| c.component.lib_reference == component_name)
.ok_or_else(|| format!("Component '{}' not found", component_name))?;
let electrical_type = parse_electrical_type(electrical)?;
let mut conglomerate = parse_pin_orientation(orientation)?;
conglomerate |= PinConglomerateFlags::DISPLAY_NAME_VISIBLE;
conglomerate |= PinConglomerateFlags::DESIGNATOR_VISIBLE;
if hidden {
conglomerate |= PinConglomerateFlags::HIDE;
}
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = mils_f64_to_raw(x_mils);
graphical.location_y = mils_f64_to_raw(y_mils);
graphical.color = 0x000080;
let pin = SchPin {
graphical,
designator: designator.to_string(),
name: name.to_string(),
electrical: electrical_type,
pin_conglomerate: conglomerate,
pin_length: mils_f64_to_raw(length_mils),
symbol_inner_edge: PinSymbol::None,
symbol_outer_edge: PinSymbol::None,
symbol_inside: PinSymbol::None,
symbol_outside: PinSymbol::None,
..Default::default()
};
component.primitives.push(SchRecord::Pin(pin));
save_schlib(path, &lib)?;
Ok(format!(
"Added pin '{}' ({}) to component '{}'",
designator, name, component_name
))
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_add_rectangle(
path: &Path,
component_name: &str,
x1: &str,
y1: &str,
x2: &str,
y2: &str,
filled: bool,
fill_color: &str,
border_color: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let mut lib = open_schlib(path)?;
let x1_mils = parse_unit_value_or_mil(x1)?;
let y1_mils = parse_unit_value_or_mil(y1)?;
let x2_mils = parse_unit_value_or_mil(x2)?;
let y2_mils = parse_unit_value_or_mil(y2)?;
let component = lib
.components
.iter_mut()
.find(|c| c.component.lib_reference == component_name)
.ok_or_else(|| format!("Component '{}' not found", component_name))?;
let fill_color_val = parse_color(fill_color)?;
let border_color_val = parse_color(border_color)?;
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = mils_f64_to_raw(x1_mils);
graphical.location_y = mils_f64_to_raw(y1_mils);
graphical.color = border_color_val;
graphical.area_color = fill_color_val;
let rect = SchRectangle {
graphical,
corner_x: mils_f64_to_raw(x2_mils),
corner_y: mils_f64_to_raw(y2_mils),
line_width: LineWidth::Small,
is_solid: filled,
transparent: !filled,
..Default::default()
};
component.primitives.push(SchRecord::Rectangle(rect));
save_schlib(path, &lib)?;
Ok(format!("Added rectangle to component '{}'", component_name))
}
pub fn cmd_add_line(
path: &Path,
component_name: &str,
x1: &str,
y1: &str,
x2: &str,
y2: &str,
color: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let mut lib = open_schlib(path)?;
let x1_mils = parse_unit_value_or_mil(x1)?;
let y1_mils = parse_unit_value_or_mil(y1)?;
let x2_mils = parse_unit_value_or_mil(x2)?;
let y2_mils = parse_unit_value_or_mil(y2)?;
let component = lib
.components
.iter_mut()
.find(|c| c.component.lib_reference == component_name)
.ok_or_else(|| format!("Component '{}' not found", component_name))?;
let color_val = parse_color(color)?;
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = mils_f64_to_raw(x1_mils);
graphical.location_y = mils_f64_to_raw(y1_mils);
graphical.color = color_val;
let line = SchLine {
graphical,
corner_x: mils_f64_to_raw(x2_mils),
corner_y: mils_f64_to_raw(y2_mils),
line_width: LineWidth::Small,
..Default::default()
};
component.primitives.push(SchRecord::Line(line));
save_schlib(path, &lib)?;
Ok(format!("Added line to component '{}'", component_name))
}
pub fn cmd_add_polygon(
path: &Path,
component_name: &str,
vertices_str: &str,
filled: bool,
fill_color: &str,
border_color: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let mut lib = open_schlib(path)?;
let component = lib
.components
.iter_mut()
.find(|c| c.component.lib_reference == component_name)
.ok_or_else(|| format!("Component '{}' not found", component_name))?;
let values: Vec<f64> = vertices_str
.split(',')
.map(|s| parse_unit_value_or_mil(s))
.collect::<Result<Vec<_>, _>>()?;
if values.len() < 6 || values.len() % 2 != 0 {
return Err("Need at least 3 vertex pairs (6 values)".into());
}
let vertices: Vec<(i32, i32)> = values
.chunks(2)
.map(|chunk| (mils_f64_to_raw(chunk[0]), mils_f64_to_raw(chunk[1])))
.collect();
let fill_color_val = parse_color(fill_color)?;
let border_color_val = parse_color(border_color)?;
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = vertices[0].0;
graphical.location_y = vertices[0].1;
graphical.color = border_color_val;
graphical.area_color = fill_color_val;
let polygon = SchPolygon {
graphical,
vertices,
line_width: LineWidth::Small,
is_solid: filled,
transparent: !filled,
..Default::default()
};
component.primitives.push(SchRecord::Polygon(polygon));
save_schlib(path, &lib)?;
Ok(format!(
"Added polygon with {} vertices to component '{}'",
values.len() / 2,
component_name
))
}
struct PinDef {
designator: String,
name: String,
electrical: PinElectricalType,
side: String,
}
fn parse_pin_defs(pins_str: &str) -> Result<Vec<PinDef>, Box<dyn std::error::Error>> {
let mut pins = Vec::new();
for pin_spec in pins_str.split(',') {
let parts: Vec<&str> = pin_spec.trim().split(':').collect();
if parts.len() < 3 {
return Err(format!(
"Invalid pin spec '{}'. Format: designator:name:type[:side]",
pin_spec
)
.into());
}
let electrical = parse_electrical_type(parts[2])?;
let side = if parts.len() > 3 {
parts[3].to_lowercase()
} else {
"left".to_string()
};
pins.push(PinDef {
designator: parts[0].to_string(),
name: parts[1].to_string(),
electrical,
side,
});
}
Ok(pins)
}
pub fn cmd_gen_ic(
path: &Path,
name: &str,
pins_str: &str,
description: Option<String>,
width: &str,
pin_length: &str,
pin_spacing: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let mut lib = open_or_create_schlib(path)?;
let width_mils = parse_unit_value_or_mil(width)?;
let pin_length_mils = parse_unit_value_or_mil(pin_length)?;
let pin_spacing_mils = parse_unit_value_or_mil(pin_spacing)?;
if lib
.components
.iter()
.any(|c| c.component.lib_reference == name)
{
return Err(format!("Component '{}' already exists", name).into());
}
let pin_defs = parse_pin_defs(pins_str)?;
let left_pins: Vec<_> = pin_defs.iter().filter(|p| p.side == "left").collect();
let right_pins: Vec<_> = pin_defs.iter().filter(|p| p.side == "right").collect();
let top_pins: Vec<_> = pin_defs.iter().filter(|p| p.side == "top").collect();
let bottom_pins: Vec<_> = pin_defs.iter().filter(|p| p.side == "bottom").collect();
let left_count = left_pins.len();
let right_count = right_pins.len();
let top_count = top_pins.len();
let bottom_count = bottom_pins.len();
let max_vertical_pins = left_count.max(right_count);
let max_horizontal_pins = top_count.max(bottom_count);
let body_height_mils = (max_vertical_pins + 1) as f64 * pin_spacing_mils;
let min_width_for_tb = if max_horizontal_pins > 0 {
(max_horizontal_pins + 1) as f64 * pin_spacing_mils
} else {
0.0
};
let width_mils = width_mils.max(min_width_for_tb);
let component = SchComponent {
lib_reference: name.to_string(),
component_description: description.unwrap_or_default(),
part_count: 1,
display_mode_count: 1,
current_part_id: 1,
..Default::default()
};
let mut primitives = vec![SchRecord::Component(component.clone())];
let mut rect_graphical = SchGraphicalBase::default();
rect_graphical.base.owner_part_id = Some(1);
rect_graphical.location_x = mils_to_raw(0);
rect_graphical.location_y = mils_to_raw(0);
rect_graphical.color = parse_color("800000")?; rect_graphical.area_color = parse_color("FFFFB0")?;
let rect = SchRectangle {
graphical: rect_graphical,
corner_x: mils_f64_to_raw(width_mils),
corner_y: mils_f64_to_raw(body_height_mils),
line_width: LineWidth::Small,
is_solid: true,
transparent: false,
..Default::default()
};
primitives.push(SchRecord::Rectangle(rect));
for (i, pin_def) in left_pins.iter().enumerate() {
let y_mils = body_height_mils - (i + 1) as f64 * pin_spacing_mils;
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = mils_f64_to_raw(-pin_length_mils);
graphical.location_y = mils_f64_to_raw(y_mils);
graphical.color = 0x000080;
let pin = SchPin {
graphical,
designator: pin_def.designator.clone(),
name: pin_def.name.clone(),
electrical: pin_def.electrical,
pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
| PinConglomerateFlags::DESIGNATOR_VISIBLE,
pin_length: mils_f64_to_raw(pin_length_mils),
..Default::default()
};
primitives.push(SchRecord::Pin(pin));
}
for (i, pin_def) in right_pins.iter().enumerate() {
let y_mils = body_height_mils - (i + 1) as f64 * pin_spacing_mils;
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = mils_f64_to_raw(width_mils + pin_length_mils);
graphical.location_y = mils_f64_to_raw(y_mils);
graphical.color = 0x000080;
let pin = SchPin {
graphical,
designator: pin_def.designator.clone(),
name: pin_def.name.clone(),
electrical: pin_def.electrical,
pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
| PinConglomerateFlags::DESIGNATOR_VISIBLE
| PinConglomerateFlags::FLIPPED,
pin_length: mils_f64_to_raw(pin_length_mils),
..Default::default()
};
primitives.push(SchRecord::Pin(pin));
}
for (i, pin_def) in top_pins.iter().enumerate() {
let x_mils = (i + 1) as f64 * pin_spacing_mils;
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = mils_f64_to_raw(x_mils);
graphical.location_y = mils_f64_to_raw(body_height_mils + pin_length_mils);
graphical.color = 0x000080;
let pin = SchPin {
graphical,
designator: pin_def.designator.clone(),
name: pin_def.name.clone(),
electrical: pin_def.electrical,
pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
| PinConglomerateFlags::DESIGNATOR_VISIBLE
| PinConglomerateFlags::ROTATED,
pin_length: mils_f64_to_raw(pin_length_mils),
..Default::default()
};
primitives.push(SchRecord::Pin(pin));
}
for (i, pin_def) in bottom_pins.iter().enumerate() {
let x_mils = (i + 1) as f64 * pin_spacing_mils;
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = mils_f64_to_raw(x_mils);
graphical.location_y = mils_f64_to_raw(-pin_length_mils);
graphical.color = 0x000080;
let pin = SchPin {
graphical,
designator: pin_def.designator.clone(),
name: pin_def.name.clone(),
electrical: pin_def.electrical,
pin_conglomerate: PinConglomerateFlags::DISPLAY_NAME_VISIBLE
| PinConglomerateFlags::DESIGNATOR_VISIBLE
| PinConglomerateFlags::ROTATED
| PinConglomerateFlags::FLIPPED,
pin_length: mils_f64_to_raw(pin_length_mils),
..Default::default()
};
primitives.push(SchRecord::Pin(pin));
}
let lib_component = SchLibComponent {
component,
primitives,
};
lib.components.push(lib_component);
save_schlib(path, &lib)?;
Ok(format!(
"Generated IC symbol '{}' with {} pins ({} left, {} right, {} top, {} bottom)",
name,
pin_defs.len(),
left_count,
right_count,
top_count,
bottom_count,
))
}
pub fn cmd_render_ascii(
path: &Path,
component_name: &str,
max_width: usize,
max_height: usize,
) -> Result<String, Box<dyn std::error::Error>> {
let lib = open_schlib(path)?;
let name_lower = component_name.to_lowercase();
let component = lib
.components
.iter()
.find(|c| c.component.lib_reference.to_lowercase() == name_lower)
.ok_or_else(|| format!("Component '{}' not found", component_name))?;
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
let mut max_x = i32::MIN;
let mut max_y = i32::MIN;
for prim in &component.primitives {
match prim {
SchRecord::Pin(p) => {
let (cx, cy) = p.get_corner();
min_x = min_x.min(p.graphical.location_x).min(cx);
min_y = min_y.min(p.graphical.location_y).min(cy);
max_x = max_x.max(p.graphical.location_x).max(cx);
max_y = max_y.max(p.graphical.location_y).max(cy);
}
SchRecord::Rectangle(r) => {
min_x = min_x.min(r.graphical.location_x).min(r.corner_x);
min_y = min_y.min(r.graphical.location_y).min(r.corner_y);
max_x = max_x.max(r.graphical.location_x).max(r.corner_x);
max_y = max_y.max(r.graphical.location_y).max(r.corner_y);
}
SchRecord::Line(l) => {
min_x = min_x.min(l.graphical.location_x).min(l.corner_x);
min_y = min_y.min(l.graphical.location_y).min(l.corner_y);
max_x = max_x.max(l.graphical.location_x).max(l.corner_x);
max_y = max_y.max(l.graphical.location_y).max(l.corner_y);
}
_ => {}
}
}
if min_x == i32::MAX {
return Ok("No renderable primitives found.".to_string());
}
let width_raw = (max_x - min_x) as f64;
let height_raw = (max_y - min_y) as f64;
let scale_x = (max_width as f64 - 2.0) / width_raw;
let scale_y = (max_height as f64 - 2.0) / height_raw;
let scale = scale_x.min(scale_y);
let canvas_width = ((width_raw * scale) as usize + 2).min(max_width);
let canvas_height = ((height_raw * scale) as usize + 2).min(max_height);
let mut canvas: Vec<Vec<char>> = vec![vec![' '; canvas_width]; canvas_height];
let to_canvas = |x: i32, y: i32| -> (usize, usize) {
let cx = ((x - min_x) as f64 * scale) as usize;
let cy = canvas_height - 1 - (((y - min_y) as f64 * scale) as usize);
(cx.min(canvas_width - 1), cy.min(canvas_height - 1))
};
for prim in &component.primitives {
if let SchRecord::Rectangle(r) = prim {
let (x1, y1) = to_canvas(r.graphical.location_x, r.graphical.location_y);
let (x2, y2) = to_canvas(r.corner_x, r.corner_y);
let (x1, x2) = (x1.min(x2), x1.max(x2));
let (y1, y2) = (y1.min(y2), y1.max(y2));
for x in x1..=x2 {
if y1 < canvas_height {
canvas[y1][x.min(canvas_width - 1)] = '-';
}
if y2 < canvas_height {
canvas[y2][x.min(canvas_width - 1)] = '-';
}
}
for y in y1..=y2 {
if x1 < canvas_width {
canvas[y.min(canvas_height - 1)][x1] = '|';
}
if x2 < canvas_width {
canvas[y.min(canvas_height - 1)][x2] = '|';
}
}
if y1 < canvas_height && x1 < canvas_width {
canvas[y1][x1] = '+';
}
if y1 < canvas_height && x2 < canvas_width {
canvas[y1][x2] = '+';
}
if y2 < canvas_height && x1 < canvas_width {
canvas[y2][x1] = '+';
}
if y2 < canvas_height && x2 < canvas_width {
canvas[y2][x2] = '+';
}
}
}
for prim in &component.primitives {
if let SchRecord::Pin(p) = prim {
let (px, py) = to_canvas(p.graphical.location_x, p.graphical.location_y);
let (cx, cy) = p.get_corner();
let (ex, ey) = to_canvas(cx, cy);
if px == ex {
let y_start = py.min(ey);
let y_end = py.max(ey);
for row in canvas.iter_mut().take(y_end + 1).skip(y_start) {
if let Some(cell) = row.get_mut(px) {
*cell = '|';
}
}
} else if let Some(row) = canvas.get_mut(py) {
let x_start = px.min(ex);
let x_end = px.max(ex);
for cell in row.iter_mut().take(x_end + 1).skip(x_start) {
*cell = '-';
}
}
if py < canvas_height && px < canvas_width {
canvas[py][px] = 'o';
}
}
}
let mut output = String::new();
output.push_str(&format!("\n{}\n", component.component.lib_reference));
output.push_str(&format!(
"{}\n",
"=".repeat(component.component.lib_reference.len())
));
for row in &canvas {
output.push_str(&format!("{}\n", row.iter().collect::<String>()));
}
output.push_str("\nPins:\n");
let mut pins: Vec<&SchPin> = component
.primitives
.iter()
.filter_map(|p| {
if let SchRecord::Pin(pin) = p {
Some(pin)
} else {
None
}
})
.collect();
pins.sort_by(|a, b| alphanumeric_sort(&a.designator, &b.designator));
for pin in pins {
output.push_str(&format!(
" {} - {} ({})\n",
pin.designator,
pin.name,
electrical_type_name(&pin.electrical)
));
}
Ok(output)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SchPinJson {
pub designator: String,
pub name: String,
pub x: CoordValue,
pub y: CoordValue,
#[serde(default = "default_pin_length")]
pub length: CoordValue,
#[serde(default = "default_electrical")]
pub electrical: String,
#[serde(default = "default_orientation")]
pub orientation: String,
#[serde(default)]
pub hidden: bool,
#[serde(default)]
pub description: String,
}
fn default_pin_length() -> CoordValue {
CoordValue(200.0)
}
fn default_electrical() -> String {
"passive".to_string()
}
fn default_orientation() -> String {
"right".to_string()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SchRectangleJson {
pub x1: CoordValue,
pub y1: CoordValue,
pub x2: CoordValue,
pub y2: CoordValue,
#[serde(default)]
pub filled: bool,
#[serde(default = "default_fill_color")]
pub fill_color: String,
#[serde(default = "default_border_color")]
pub border_color: String,
}
fn default_fill_color() -> String {
"FFFFB0".to_string()
}
fn default_border_color() -> String {
"000080".to_string()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SchLineJson {
pub x1: CoordValue,
pub y1: CoordValue,
pub x2: CoordValue,
pub y2: CoordValue,
#[serde(default = "default_border_color")]
pub color: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SchPolygonJson {
pub vertices: Vec<[CoordValue; 2]>,
#[serde(default)]
pub filled: bool,
#[serde(default = "default_fill_color")]
pub fill_color: String,
#[serde(default = "default_border_color")]
pub border_color: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SchLabelJson {
pub x: CoordValue,
pub y: CoordValue,
pub text: String,
#[serde(default = "default_label_orientation")]
pub orientation: String,
#[serde(default = "default_justification")]
pub justification: String,
#[serde(default = "default_border_color")]
pub color: String,
#[serde(default = "default_font_id")]
pub font_id: i32,
#[serde(default)]
pub hidden: bool,
}
fn default_label_orientation() -> String {
"horizontal".to_string()
}
fn default_justification() -> String {
"bottom_left".to_string()
}
fn default_font_id() -> i32 {
1
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SchArcJson {
pub x: CoordValue,
pub y: CoordValue,
pub radius: CoordValue,
#[serde(default)]
pub start_angle: f64,
#[serde(default = "default_end_angle")]
pub end_angle: f64,
#[serde(default = "default_border_color")]
pub color: String,
}
fn default_end_angle() -> f64 {
360.0
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SchPolylineJson {
pub vertices: Vec<[CoordValue; 2]>,
#[serde(default = "default_border_color")]
pub color: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SchEllipseJson {
pub x: CoordValue,
pub y: CoordValue,
pub radius_x: CoordValue,
pub radius_y: CoordValue,
#[serde(default)]
pub filled: bool,
#[serde(default = "default_fill_color")]
pub fill_color: String,
#[serde(default = "default_border_color")]
pub border_color: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SchComponentJson {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default = "default_part_count")]
pub part_count: i32,
#[serde(default)]
pub pins: Vec<SchPinJson>,
#[serde(default)]
pub rectangles: Vec<SchRectangleJson>,
#[serde(default)]
pub lines: Vec<SchLineJson>,
#[serde(default)]
pub polygons: Vec<SchPolygonJson>,
#[serde(default)]
pub labels: Vec<SchLabelJson>,
#[serde(default)]
pub arcs: Vec<SchArcJson>,
#[serde(default)]
pub polylines: Vec<SchPolylineJson>,
#[serde(default)]
pub ellipses: Vec<SchEllipseJson>,
}
fn default_part_count() -> i32 {
1
}
pub fn cmd_add_json(
path: &Path,
json_file: Option<String>,
json_str: Option<String>,
) -> Result<String, Box<dyn std::error::Error>> {
use std::io::{self, Read as IoRead};
let json_content = match (json_file, json_str) {
(_, Some(s)) => s,
(Some(ref path), None) if path == "-" => {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
buffer
}
(Some(ref file_path), None) => std::fs::read_to_string(file_path)?,
(None, None) => {
return Err("Must provide either --file <file> or --json <string>".into());
}
};
let component_def: SchComponentJson = serde_json::from_str(&json_content)?;
let mut lib = open_or_create_schlib(path)?;
if lib
.components
.iter()
.any(|c| c.component.lib_reference == component_def.name)
{
return Err(format!("Component '{}' already exists", component_def.name).into());
}
let component = SchComponent {
lib_reference: component_def.name.clone(),
component_description: component_def.description.clone(),
part_count: component_def.part_count,
display_mode_count: 1,
current_part_id: 1,
..Default::default()
};
let mut primitives = vec![SchRecord::Component(component.clone())];
for rect in &component_def.rectangles {
let fill_color_val = parse_color(&rect.fill_color)?;
let border_color_val = parse_color(&rect.border_color)?;
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = rect.x1.to_raw();
graphical.location_y = rect.y1.to_raw();
graphical.color = border_color_val;
graphical.area_color = fill_color_val;
let sch_rect = SchRectangle {
graphical,
corner_x: rect.x2.to_raw(),
corner_y: rect.y2.to_raw(),
line_width: LineWidth::Small,
is_solid: rect.filled,
transparent: !rect.filled,
..Default::default()
};
primitives.push(SchRecord::Rectangle(sch_rect));
}
for line in &component_def.lines {
let color_val = parse_color(&line.color)?;
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = line.x1.to_raw();
graphical.location_y = line.y1.to_raw();
graphical.color = color_val;
let sch_line = SchLine {
graphical,
corner_x: line.x2.to_raw(),
corner_y: line.y2.to_raw(),
line_width: LineWidth::Small,
..Default::default()
};
primitives.push(SchRecord::Line(sch_line));
}
for polygon in &component_def.polygons {
if polygon.vertices.len() < 3 {
return Err("Polygon must have at least 3 vertices".into());
}
let fill_color_val = parse_color(&polygon.fill_color)?;
let border_color_val = parse_color(&polygon.border_color)?;
let vertices: Vec<(i32, i32)> = polygon
.vertices
.iter()
.map(|v| (v[0].to_raw(), v[1].to_raw()))
.collect();
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = vertices[0].0;
graphical.location_y = vertices[0].1;
graphical.color = border_color_val;
graphical.area_color = fill_color_val;
let sch_polygon = SchPolygon {
graphical,
vertices,
line_width: LineWidth::Small,
is_solid: polygon.filled,
transparent: !polygon.filled,
..Default::default()
};
primitives.push(SchRecord::Polygon(sch_polygon));
}
for label in &component_def.labels {
let color_val = parse_color(&label.color)?;
let orientation = parse_text_orientation(&label.orientation)?;
let justification = parse_text_justification(&label.justification)?;
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = label.x.to_raw();
graphical.location_y = label.y.to_raw();
graphical.color = color_val;
let sch_label = SchLabel {
graphical,
text: label.text.clone(),
orientation,
justification,
font_id: label.font_id,
is_hidden: label.hidden,
..Default::default()
};
primitives.push(SchRecord::Label(sch_label));
}
for arc in &component_def.arcs {
let color_val = parse_color(&arc.color)?;
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = arc.x.to_raw();
graphical.location_y = arc.y.to_raw();
graphical.color = color_val;
let sch_arc = SchArc {
graphical,
radius: arc.radius.to_raw(),
secondary_radius: arc.radius.to_raw(), start_angle: arc.start_angle,
end_angle: arc.end_angle,
line_width: LineWidth::Small,
..Default::default()
};
primitives.push(SchRecord::Arc(sch_arc));
}
for polyline in &component_def.polylines {
if polyline.vertices.len() < 2 {
return Err("Polyline must have at least 2 vertices".into());
}
let color_val = parse_color(&polyline.color)?;
let vertices: Vec<(i32, i32)> = polyline
.vertices
.iter()
.map(|v| (v[0].to_raw(), v[1].to_raw()))
.collect();
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = vertices[0].0;
graphical.location_y = vertices[0].1;
graphical.color = color_val;
let sch_polyline = SchPolyline {
graphical,
vertices,
line_width: LineWidth::Small,
..Default::default()
};
primitives.push(SchRecord::Polyline(sch_polyline));
}
for ellipse in &component_def.ellipses {
let fill_color_val = parse_color(&ellipse.fill_color)?;
let border_color_val = parse_color(&ellipse.border_color)?;
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = ellipse.x.to_raw();
graphical.location_y = ellipse.y.to_raw();
graphical.color = border_color_val;
graphical.area_color = fill_color_val;
let sch_ellipse = SchEllipse {
graphical,
radius_x: ellipse.radius_x.to_raw(),
radius_y: ellipse.radius_y.to_raw(),
is_solid: ellipse.filled,
transparent: !ellipse.filled,
line_width: LineWidth::Small,
..Default::default()
};
primitives.push(SchRecord::Ellipse(sch_ellipse));
}
for pin_def in &component_def.pins {
let electrical_type = parse_electrical_type(&pin_def.electrical)?;
let mut conglomerate = parse_pin_orientation(&pin_def.orientation)?;
conglomerate |= PinConglomerateFlags::DISPLAY_NAME_VISIBLE;
conglomerate |= PinConglomerateFlags::DESIGNATOR_VISIBLE;
if pin_def.hidden {
conglomerate |= PinConglomerateFlags::HIDE;
}
let mut graphical = SchGraphicalBase::default();
graphical.base.owner_part_id = Some(1);
graphical.location_x = pin_def.x.to_raw();
graphical.location_y = pin_def.y.to_raw();
graphical.color = 0x000080;
let pin = SchPin {
graphical,
designator: pin_def.designator.clone(),
name: pin_def.name.clone(),
electrical: electrical_type,
pin_conglomerate: conglomerate,
pin_length: pin_def.length.to_raw(),
description: pin_def.description.clone(),
symbol_inner_edge: PinSymbol::None,
symbol_outer_edge: PinSymbol::None,
symbol_inside: PinSymbol::None,
symbol_outside: PinSymbol::None,
..Default::default()
};
primitives.push(SchRecord::Pin(pin));
}
let pin_count = component_def.pins.len();
let rect_count = component_def.rectangles.len();
let line_count = component_def.lines.len();
let polygon_count = component_def.polygons.len();
let label_count = component_def.labels.len();
let arc_count = component_def.arcs.len();
let polyline_count = component_def.polylines.len();
let ellipse_count = component_def.ellipses.len();
let lib_component = SchLibComponent {
component,
primitives,
};
lib.components.push(lib_component);
save_schlib(path, &lib)?;
let mut parts = vec![format!("{} pins", pin_count)];
if rect_count > 0 {
parts.push(format!("{} rectangles", rect_count));
}
if line_count > 0 {
parts.push(format!("{} lines", line_count));
}
if polygon_count > 0 {
parts.push(format!("{} polygons", polygon_count));
}
if label_count > 0 {
parts.push(format!("{} labels", label_count));
}
if arc_count > 0 {
parts.push(format!("{} arcs", arc_count));
}
if polyline_count > 0 {
parts.push(format!("{} polylines", polyline_count));
}
if ellipse_count > 0 {
parts.push(format!("{} ellipses", ellipse_count));
}
Ok(format!(
"Added component '{}' with {} to {}",
component_def.name,
parts.join(", "),
path.display()
))
}
fn parse_text_orientation(s: &str) -> Result<TextOrientations, Box<dyn std::error::Error>> {
match s.to_lowercase().as_str() {
"horizontal" | "0" => Ok(TextOrientations::NONE),
"vertical_up" | "90" | "up" => Ok(TextOrientations::ROTATED),
"vertical_down" | "270" | "down" => Ok(TextOrientations::ROTATED | TextOrientations::FLIPPED),
"180" | "flipped" => Ok(TextOrientations::FLIPPED),
_ => Err(format!("Unknown text orientation: {}. Use: horizontal, vertical_up, vertical_down, 90, 180, 270", s).into()),
}
}
fn parse_text_justification(s: &str) -> Result<TextJustification, Box<dyn std::error::Error>> {
match s.to_lowercase().replace('_', "").as_str() {
"bottomleft" | "bl" => Ok(TextJustification::BOTTOM_LEFT),
"bottomcenter" | "bc" => Ok(TextJustification::BOTTOM_CENTER),
"bottomright" | "br" => Ok(TextJustification::BOTTOM_RIGHT),
"centerleft" | "cl" | "middleleft" | "ml" => Ok(TextJustification::MIDDLE_LEFT),
"center" | "c" | "middle" | "m" => Ok(TextJustification::MIDDLE_CENTER),
"centerright" | "cr" | "middleright" | "mr" => Ok(TextJustification::MIDDLE_RIGHT),
"topleft" | "tl" => Ok(TextJustification::TOP_LEFT),
"topcenter" | "tc" => Ok(TextJustification::TOP_CENTER),
"topright" | "tr" => Ok(TextJustification::TOP_RIGHT),
_ => Err(format!("Unknown justification: {}. Use: bottom_left, bottom_center, bottom_right, center_left, center, center_right, top_left, top_center, top_right", s).into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn temp_schlib() -> PathBuf {
let id = uuid::Uuid::new_v4();
std::env::temp_dir().join(format!("test_{}.SchLib", id))
}
#[test]
fn test_gen_ic_all_four_sides() {
let path = temp_schlib();
cmd_create(&path).unwrap();
let result = cmd_gen_ic(
&path,
"TEST_4SIDE",
"1:VCC:power:top,2:GND:power:bottom,3:IN:input:left,4:OUT:output:right",
Some("4-side test".to_string()),
"600mil",
"200mil",
"100mil",
)
.unwrap();
assert!(result.contains("4 pins"), "Expected 4 pins, got: {}", result);
assert!(result.contains("1 left"), "Expected 1 left pin: {}", result);
assert!(result.contains("1 right"), "Expected 1 right pin: {}", result);
assert!(result.contains("1 top"), "Expected 1 top pin: {}", result);
assert!(result.contains("1 bottom"), "Expected 1 bottom pin: {}", result);
let lib = open_or_create_schlib(&path).unwrap();
let comp = lib
.components
.iter()
.find(|c| c.component.lib_reference == "TEST_4SIDE")
.expect("Component must exist");
let pin_count = comp
.primitives
.iter()
.filter(|r| matches!(r, SchRecord::Pin(_)))
.count();
assert_eq!(pin_count, 4, "All 4 pins must be saved, got {}", pin_count);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_gen_ic_only_top_bottom() {
let path = temp_schlib();
cmd_create(&path).unwrap();
let result = cmd_gen_ic(
&path,
"PWR_2PIN",
"1:VCC:power:top,2:GND:power:bottom",
None,
"400mil",
"200mil",
"100mil",
)
.unwrap();
assert!(result.contains("2 pins"), "Expected 2 pins, got: {}", result);
assert!(result.contains("1 top"), "Expected 1 top: {}", result);
assert!(result.contains("1 bottom"), "Expected 1 bottom: {}", result);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_gen_ic_multi_pin_sides() {
let path = temp_schlib();
cmd_create(&path).unwrap();
let pins = [
"1:A:io:left", "2:B:io:left", "3:C:io:left",
"4:D:io:right", "5:E:io:right", "6:F:io:right", "7:G:io:right",
"8:VCC:power:top", "9:VCC2:power:top",
"10:GND:power:bottom", "11:GND2:power:bottom",
]
.join(",");
let result = cmd_gen_ic(
&path,
"MULTI_PIN",
&pins,
None,
"800mil",
"200mil",
"100mil",
)
.unwrap();
assert!(result.contains("11 pins"), "Expected 11 pins, got: {}", result);
assert!(result.contains("3 left"), "Got: {}", result);
assert!(result.contains("4 right"), "Got: {}", result);
assert!(result.contains("2 top"), "Got: {}", result);
assert!(result.contains("2 bottom"), "Got: {}", result);
let lib = open_or_create_schlib(&path).unwrap();
let comp = lib
.components
.iter()
.find(|c| c.component.lib_reference == "MULTI_PIN")
.unwrap();
let pin_count = comp
.primitives
.iter()
.filter(|r| matches!(r, SchRecord::Pin(_)))
.count();
assert_eq!(pin_count, 11);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_gen_ic_body_widens_for_top_bottom() {
let path = temp_schlib();
cmd_create(&path).unwrap();
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";
cmd_gen_ic(
&path,
"WIDE_TOP",
pins,
None,
"400mil", "200mil",
"100mil",
)
.unwrap();
let lib = open_or_create_schlib(&path).unwrap();
let comp = lib
.components
.iter()
.find(|c| c.component.lib_reference == "WIDE_TOP")
.unwrap();
let rect = comp.primitives.iter().find_map(|r| {
if let SchRecord::Rectangle(rect) = r {
Some(rect)
} else {
None
}
}).expect("Must have body rectangle");
let body_width_mils = rect.corner_x as f64 / 10000.0;
assert!(
body_width_mils >= 600.0,
"Body width should expand to at least 600mil for 5 top pins, got {}mil",
body_width_mils
);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_gen_ic_invalid_pin_type() {
let path = temp_schlib();
cmd_create(&path).unwrap();
let result = cmd_gen_ic(
&path,
"BAD",
"1:VCC:W:left", None,
"400mil",
"200mil",
"100mil",
);
assert!(result.is_err(), "Invalid pin type should fail");
let err = result.unwrap_err().to_string();
assert!(
err.contains("Unknown electrical type"),
"Error should mention electrical type, got: {}",
err
);
std::fs::remove_file(&path).ok();
}
}