use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Cursor};
use std::path::{Path, PathBuf};
use png;
use resvg;
use serde::{Deserialize, Serialize};
use serde_json;
use crate::dump::fmt_coord_val;
use crate::footprint::{
AsciiOptions, FootprintBuilder, PadRowDirection, SvgOptions, render_ascii, render_svg,
};
use crate::io::PcbLib;
use crate::records::pcb::{PcbPad, PcbPadShape, PcbRecord, PcbText};
use crate::types::{Layer, Unit};
use super::util::alphanumeric_sort;
use crate::ops::output::*;
fn open_pcblib(path: &Path) -> Result<PcbLib, Box<dyn std::error::Error>> {
let file = File::open(path)?;
Ok(PcbLib::open(BufReader::new(file))?)
}
fn categorize_footprint(pattern: &str, description: &str) -> &'static str {
let pattern_lower = pattern.to_lowercase();
let desc_lower = description.to_lowercase();
if pattern_lower.contains("qfp")
|| pattern_lower.contains("tqfp")
|| pattern_lower.contains("lqfp")
{
return "QFP";
}
if pattern_lower.contains("qfn")
|| pattern_lower.contains("dfn")
|| pattern_lower.contains("mlf")
{
return "QFN/DFN";
}
if pattern_lower.contains("bga")
|| pattern_lower.contains("csbga")
|| pattern_lower.contains("wlcsp")
{
return "BGA";
}
if pattern_lower.contains("soic")
|| pattern_lower.contains("so-")
|| pattern_lower.contains("sop")
{
return "SOIC/SOP";
}
if pattern_lower.contains("ssop")
|| pattern_lower.contains("tssop")
|| pattern_lower.contains("msop")
{
return "SSOP/TSSOP";
}
if pattern_lower.contains("sot") {
return "SOT";
}
if pattern_lower.contains("dip") || pattern_lower.contains("pdip") {
return "DIP";
}
if pattern_lower.contains("to-")
|| pattern_lower.contains("to2")
|| pattern_lower.contains("to3")
|| pattern_lower.contains("dpak")
|| pattern_lower.contains("d2pak")
{
return "TO/DPAK";
}
if pattern_lower.starts_with("0402")
|| pattern_lower.starts_with("0603")
|| pattern_lower.starts_with("0805")
|| pattern_lower.starts_with("1206")
|| pattern_lower.starts_with("1210")
|| pattern_lower.starts_with("0201")
|| pattern_lower.starts_with("1812")
|| pattern_lower.starts_with("2010")
|| pattern_lower.starts_with("2512")
{
return "Chip (SMD)";
}
if pattern_lower.contains("cap") || desc_lower.contains("capacitor") {
return "Capacitor";
}
if pattern_lower.contains("res") || desc_lower.contains("resistor") {
return "Resistor";
}
if pattern_lower.contains("ind")
|| pattern_lower.contains("ferrite")
|| desc_lower.contains("inductor")
{
return "Inductor";
}
if pattern_lower.contains("header")
|| pattern_lower.contains("conn")
|| pattern_lower.contains("socket")
|| pattern_lower.contains("pin")
|| pattern_lower.contains("terminal")
{
return "Connector";
}
if pattern_lower.contains("usb") {
return "USB";
}
if pattern_lower.contains("rj45") || pattern_lower.contains("ethernet") {
return "RJ45/Ethernet";
}
if pattern_lower.contains("diode")
|| pattern_lower.contains("sod")
|| pattern_lower.contains("sma")
|| pattern_lower.contains("smb")
|| pattern_lower.contains("smc")
{
return "Diode";
}
if pattern_lower.contains("led") {
return "LED";
}
if pattern_lower.contains("crystal")
|| pattern_lower.contains("xtal")
|| pattern_lower.contains("osc")
{
return "Crystal/Oscillator";
}
if pattern_lower.contains("test") || pattern_lower.contains("tp") {
return "Test Point";
}
if pattern_lower.contains("th")
|| pattern_lower.contains("axial")
|| pattern_lower.contains("radial")
{
return "Through-Hole";
}
"Other"
}
fn pad_shape_name(shape: PcbPadShape) -> &'static str {
shape.name()
}
fn record_type_name(record: &PcbRecord) -> &'static str {
match record {
PcbRecord::Arc(_) => "Arc",
PcbRecord::Pad(_) => "Pad",
PcbRecord::Via(_) => "Via",
PcbRecord::Track(_) => "Track",
PcbRecord::Text(_) => "Text",
PcbRecord::Fill(_) => "Fill",
PcbRecord::Region(_) => "Region",
PcbRecord::ComponentBody(_) => "ComponentBody",
PcbRecord::Polygon(_) => "Polygon",
PcbRecord::Unknown { .. } => "Unknown",
}
}
fn layer_name(layer: &Layer) -> String {
match layer.to_byte() {
1 => "Top".to_string(),
32 => "Bottom".to_string(),
74 => "Multi".to_string(),
_ => format!("L{}", layer.to_byte()),
}
}
pub fn cmd_overview(path: &Path) -> Result<PcbLibOverview, Box<dyn std::error::Error>> {
let lib = open_pcblib(path)?;
let mut categories: HashMap<&'static str, Vec<FootprintSummaryExt>> = HashMap::new();
for comp in lib.iter() {
let category = categorize_footprint(&comp.pattern, &comp.description);
categories
.entry(category)
.or_default()
.push(FootprintSummaryExt {
name: comp.pattern.clone(),
description: comp.description.clone(),
pad_count: comp.pad_count(),
});
}
let category_order = [
"QFP",
"QFN/DFN",
"BGA",
"SOIC/SOP",
"SSOP/TSSOP",
"SOT",
"DIP",
"TO/DPAK",
"Chip (SMD)",
"Capacitor",
"Resistor",
"Inductor",
"Connector",
"USB",
"RJ45/Ethernet",
"Diode",
"LED",
"Crystal/Oscillator",
"Test Point",
"Through-Hole",
"Other",
];
let mut footprints_by_category = Vec::new();
for category in category_order.iter() {
if let Some(footprints) = categories.remove(*category) {
footprints_by_category.push((category.to_string(), footprints));
}
}
for (category, footprints) in categories {
if !footprints.is_empty() {
footprints_by_category.push((category.to_string(), footprints));
}
}
let mut total_pads = 0;
let mut smd_pads = 0;
let mut th_pads = 0;
let mut pad_shapes: HashMap<&'static str, usize> = HashMap::new();
for comp in lib.iter() {
for pad in comp.pads() {
total_pads += 1;
if pad.has_hole() {
th_pads += 1;
} else {
smd_pads += 1;
}
*pad_shapes
.entry(pad_shape_name(pad.shape_top()))
.or_insert(0) += 1;
}
}
let mut pad_shapes_vec: Vec<_> = pad_shapes
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
pad_shapes_vec.sort_by(|a, b| b.1.cmp(&a.1));
let mut hole_sizes: HashMap<String, usize> = HashMap::new();
for comp in lib.iter() {
for pad in comp.pads() {
if pad.has_hole() && pad.hole_size.to_raw() > 0 {
let size_str = fmt_coord_val(&pad.hole_size);
*hole_sizes.entry(size_str).or_insert(0) += 1;
}
}
}
let mut hole_sizes_vec: Vec<_> = hole_sizes.into_iter().collect();
hole_sizes_vec.sort_by(|a, b| b.1.cmp(&a.1));
let mut by_pads: Vec<_> = lib.iter().collect();
by_pads.sort_by_key(|b| std::cmp::Reverse(b.pad_count()));
let largest_footprints = by_pads
.iter()
.take(10)
.map(|comp| FootprintSummaryExt {
name: comp.pattern.clone(),
description: comp.description.clone(),
pad_count: comp.pad_count(),
})
.collect();
Ok(PcbLibOverview {
path: path.display().to_string(),
total_footprints: lib.components.len(),
unique_id: lib.unique_id.clone(),
footprints_by_category,
pad_statistics: PadStatistics {
total_pads,
smd_pads,
th_pads,
pad_shapes: pad_shapes_vec,
},
hole_sizes: hole_sizes_vec.into_iter().take(10).collect(),
largest_footprints,
})
}
pub fn cmd_list(path: &Path) -> Result<PcbLibFootprintList, Box<dyn std::error::Error>> {
let lib = open_pcblib(path)?;
let footprints = lib
.iter()
.map(|comp| FootprintSummaryExt {
name: comp.pattern.clone(),
description: comp.description.clone(),
pad_count: comp.pad_count(),
})
.collect();
Ok(PcbLibFootprintList {
path: path.display().to_string(),
total_footprints: lib.components.len(),
footprints,
})
}
pub fn cmd_search(
path: &Path,
query: &str,
) -> Result<PcbLibSearchResults, Box<dyn std::error::Error>> {
let lib = open_pcblib(path)?;
let query_lower = query.to_lowercase();
let has_wildcard = query.contains('*');
let matches: Vec<_> = lib
.iter()
.filter(|comp| {
let name = comp.pattern.to_lowercase();
let desc = comp.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| FootprintSummaryExt {
name: comp.pattern.clone(),
description: comp.description.clone(),
pad_count: comp.pad_count(),
})
.collect();
Ok(PcbLibSearchResults {
query: query.to_string(),
total_matches: matches.len(),
results: matches,
})
}
pub fn cmd_info(path: &Path) -> Result<PcbLibInfo, Box<dyn std::error::Error>> {
let lib = open_pcblib(path)?;
let mut primitive_counts: HashMap<&'static str, usize> = HashMap::new();
let mut total_primitives = 0;
for comp in lib.iter() {
for prim in &comp.primitives {
let name = record_type_name(prim);
*primitive_counts.entry(name).or_insert(0) += 1;
total_primitives += 1;
}
}
let mut primitive_types: Vec<_> = primitive_counts
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
primitive_types.sort_by(|a, b| b.1.cmp(&a.1));
Ok(PcbLibInfo {
path: path.display().to_string(),
footprint_count: lib.components.len(),
unique_id: lib.unique_id.clone(),
total_primitives,
primitive_types,
})
}
pub fn cmd_footprint(
path: &Path,
name: &str,
show_primitives: bool,
) -> Result<PcbLibFootprintDetail, Box<dyn std::error::Error>> {
let lib = open_pcblib(path)?;
let name_lower = name.to_lowercase();
let comp = lib
.iter()
.find(|c| c.pattern.to_lowercase() == name_lower)
.ok_or_else(|| format!("Footprint '{}' not found", name))?;
let bounds = comp.calculate_bounds();
let mut pads: Vec<&PcbPad> = comp.pads().collect();
pads.sort_by(|a, b| alphanumeric_sort(&a.designator, &b.designator));
let pad_details = pads
.iter()
.map(|pad| {
let size = pad.size_top();
let size_str = format!("{}x{}", fmt_coord_val(&size.x), fmt_coord_val(&size.y));
let hole_str = if pad.has_hole() {
Some(fmt_coord_val(&pad.hole_size))
} else {
None
};
PadDetail {
designator: pad.designator.clone(),
shape: pad_shape_name(pad.shape_top()).to_string(),
size: size_str,
hole: hole_str,
layer: layer_name(&pad.common.layer),
}
})
.collect();
let primitive_counts = if show_primitives {
let mut prim_counts: HashMap<&'static str, usize> = HashMap::new();
for prim in &comp.primitives {
*prim_counts.entry(record_type_name(prim)).or_insert(0) += 1;
}
let mut counts: Vec<_> = prim_counts
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
counts.sort_by(|a, b| b.1.cmp(&a.1));
Some(counts)
} else {
None
};
Ok(PcbLibFootprintDetail {
pattern: comp.pattern.clone(),
description: comp.description.clone(),
height: if comp.height.to_raw() > 0 {
fmt_coord_val(&comp.height)
} else {
String::new()
},
pad_count: comp.pad_count(),
total_primitives: comp.primitive_count(),
bounding_box: BoundingBox {
width: fmt_coord_val(&bounds.width()),
height: fmt_coord_val(&bounds.height()),
},
pads: pad_details,
primitive_counts,
})
}
pub fn cmd_pads(
path: &Path,
footprint_filter: Option<String>,
by_shape: bool,
) -> Result<PcbLibPadList, Box<dyn std::error::Error>> {
let lib = open_pcblib(path)?;
let filter_lower = footprint_filter.as_ref().map(|s| s.to_lowercase());
let mut all_pads: Vec<PadWithFootprint> = Vec::new();
for comp in lib.iter() {
if let Some(ref filter) = filter_lower {
if !comp.pattern.to_lowercase().contains(filter) {
continue;
}
}
for pad in comp.pads() {
let size = pad.size_top();
let size_str = format!("{}x{}", fmt_coord_val(&size.x), fmt_coord_val(&size.y));
let hole_str = if pad.has_hole() {
Some(fmt_coord_val(&pad.hole_size))
} else {
None
};
all_pads.push(PadWithFootprint {
footprint_name: comp.pattern.clone(),
designator: pad.designator.clone(),
size: size_str,
hole: hole_str,
shape: pad_shape_name(pad.shape_top()).to_string(),
});
}
}
let pads_by_shape = if by_shape {
let mut by_shape: HashMap<String, Vec<PadWithFootprint>> = HashMap::new();
for pad in &all_pads {
by_shape
.entry(pad.shape.clone())
.or_default()
.push(pad.clone());
}
let shape_order = ["Round", "Rectangular", "Rounded Rect", "Octagonal"];
let mut result = Vec::new();
for shape in shape_order {
if let Some(pads) = by_shape.remove(shape) {
result.push((shape.to_string(), pads));
}
}
for (shape, pads) in by_shape {
result.push((shape, pads));
}
Some(result)
} else {
None
};
Ok(PcbLibPadList {
path: path.display().to_string(),
total_pads: all_pads.len(),
pads: all_pads,
pads_by_shape,
})
}
pub fn cmd_primitives(
path: &Path,
name: &str,
) -> Result<PcbLibPrimitiveList, Box<dyn std::error::Error>> {
let lib = open_pcblib(path)?;
let name_lower = name.to_lowercase();
let comp = lib
.iter()
.find(|c| c.pattern.to_lowercase() == name_lower)
.ok_or_else(|| format!("Footprint '{}' not found", name))?;
let primitives = comp
.primitives
.iter()
.map(|prim| match prim {
PcbRecord::Pad(p) => {
let size = p.size_top();
let hole = if p.has_hole() {
Some(fmt_coord_val(&p.hole_size))
} else {
None
};
PrimitiveDetail::Pad {
designator: p.designator.clone(),
shape: pad_shape_name(p.shape_top()).to_string(),
size: format!("{}x{}", fmt_coord_val(&size.x), fmt_coord_val(&size.y)),
hole,
}
}
PcbRecord::Track(t) => PrimitiveDetail::Track {
start_x: fmt_coord_val(&t.start.x),
start_y: fmt_coord_val(&t.start.y),
end_x: fmt_coord_val(&t.end.x),
end_y: fmt_coord_val(&t.end.y),
width: fmt_coord_val(&t.width),
},
PcbRecord::Arc(a) => PrimitiveDetail::Arc {
center_x: fmt_coord_val(&a.location.x),
center_y: fmt_coord_val(&a.location.y),
radius: fmt_coord_val(&a.radius),
start_angle: a.start_angle,
end_angle: a.end_angle,
},
PcbRecord::Text(t) => PrimitiveDetail::Text {
text: t.text.clone(),
x: fmt_coord_val(&t.base.corner1.x),
y: fmt_coord_val(&t.base.corner1.y),
},
PcbRecord::Fill(f) => PrimitiveDetail::Fill {
x1: fmt_coord_val(&f.base.corner1.x),
y1: fmt_coord_val(&f.base.corner1.y),
x2: fmt_coord_val(&f.base.corner2.x),
y2: fmt_coord_val(&f.base.corner2.y),
},
PcbRecord::Region(r) => PrimitiveDetail::Region {
vertex_count: r.outline.len(),
layer: layer_name(&r.common.layer),
},
PcbRecord::ComponentBody(b) => PrimitiveDetail::ComponentBody {
vertex_count: b.outline.len(),
height: fmt_coord_val(&b.overall_height),
},
_ => PrimitiveDetail::Other {
primitive_type: record_type_name(prim).to_string(),
},
})
.collect();
Ok(PcbLibPrimitiveList {
footprint_name: comp.pattern.clone(),
total_primitives: comp.primitive_count(),
primitives,
})
}
pub fn cmd_holes(path: &Path) -> Result<PcbLibHoleAnalysis, Box<dyn std::error::Error>> {
let lib = open_pcblib(path)?;
let mut hole_sizes: HashMap<String, Vec<String>> = HashMap::new();
for comp in lib.iter() {
for pad in comp.pads() {
if pad.has_hole() && pad.hole_size.to_raw() > 0 {
let size_str = fmt_coord_val(&pad.hole_size);
hole_sizes
.entry(size_str)
.or_default()
.push(comp.pattern.clone());
}
}
}
let mut hole_size_infos: Vec<_> = hole_sizes
.into_iter()
.map(|(size, footprints)| {
let unique_footprints: std::collections::HashSet<_> = footprints.into_iter().collect();
let count = unique_footprints.len();
let example_footprints: Vec<_> = unique_footprints.into_iter().take(3).collect();
HoleSizeInfo {
size,
count,
example_footprints,
}
})
.collect();
hole_size_infos.sort_by(|a, b| b.count.cmp(&a.count));
Ok(PcbLibHoleAnalysis {
path: path.display().to_string(),
hole_sizes: hole_size_infos,
})
}
pub fn cmd_json(path: &Path, full: bool) -> Result<PcbLibJson, Box<dyn std::error::Error>> {
let lib = open_pcblib(path)?;
let footprints: Vec<FootprintJsonData> = lib
.iter()
.map(|comp| {
let pads = if full {
Some(
comp.pads()
.map(|pad| {
let size = pad.size_top();
PadJsonData {
designator: pad.designator.clone(),
shape: pad_shape_name(pad.shape_top()).to_string(),
size_x: fmt_coord_val(&size.x),
size_y: fmt_coord_val(&size.y),
hole_size: if pad.has_hole() {
Some(fmt_coord_val(&pad.hole_size))
} else {
None
},
layer: layer_name(&pad.common.layer),
}
})
.collect(),
)
} else {
None
};
FootprintJsonData {
name: comp.pattern.clone(),
description: comp.description.clone(),
pad_count: comp.pad_count(),
primitive_count: comp.primitive_count(),
pads,
}
})
.collect();
Ok(PcbLibJson {
file: path.display().to_string(),
footprint_count: lib.components.len(),
unique_id: lib.unique_id.clone(),
footprints,
})
}
use crate::footprint::{
Measurement, analyze_pitch, generate_report, measure_dimensions, measure_pad,
measure_pad_distance, minimum_pad_clearance, pad_to_silkscreen_clearance,
};
pub fn cmd_measure(
path: &Path,
name: &str,
measure_type: &str,
pad1: Option<String>,
pad2: Option<String>,
pad: Option<String>,
output_json: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let lib = open_pcblib(path)?;
let name_lower = name.to_lowercase();
let component = lib
.iter()
.find(|c| c.pattern.to_lowercase() == name_lower || matches_pattern(&c.pattern, name))
.ok_or_else(|| format!("Footprint '{}' not found", name))?;
match measure_type.to_lowercase().as_str() {
"all" | "report" => {
let report = generate_report(component);
if output_json {
print_measurement_report_json(&report)?;
} else {
print_measurement_report(&report);
}
}
"distance" | "dist" => {
let p1 = pad1.ok_or("--pad1 required for distance measurement")?;
let p2 = pad2.ok_or("--pad2 required for distance measurement")?;
let dist = measure_pad_distance(component, &p1, &p2).ok_or_else(|| {
format!("Could not measure distance between pads {} and {}", p1, p2)
})?;
if output_json {
print_distance_json(&dist)?;
} else {
println!("Distance: {} to {}", dist.pad1, dist.pad2);
println!(" Center-to-center: {}", dist.center_to_center.display());
println!(" Edge-to-edge: {}", dist.edge_to_edge.display());
}
}
"pitch" => {
let pitches = analyze_pitch(component);
if output_json {
print_pitch_json(&pitches)?;
} else if pitches.is_empty() {
println!("No regular pitch detected (footprint may have irregular pad spacing)");
} else {
println!("Pitch Analysis for: {}", component.pattern);
println!("═══════════════════════════════════════════════════════════════");
for pitch_info in &pitches {
println!(
"\n{} pitch: {}",
pitch_info.direction,
pitch_info.pitch.display()
);
println!(" {} pad pairs with this spacing", pitch_info.count);
for (p1, p2, dist) in pitch_info.pad_pairs.iter().take(5) {
println!(" {} ↔ {}: {}", p1, p2, dist.display());
}
if pitch_info.pad_pairs.len() > 5 {
println!(" ... and {} more pairs", pitch_info.pad_pairs.len() - 5);
}
}
}
}
"dimensions" | "dims" | "bounds" => {
let dims = measure_dimensions(component);
if output_json {
print_dimensions_json(&dims)?;
} else {
println!("Dimensions for: {}", component.pattern);
println!("═══════════════════════════════════════════════════════════════");
println!(" Width: {}", dims.width.display());
println!(" Height: {}", dims.height.display());
println!(
" X range: {} to {}",
dims.min_x.display(),
dims.max_x.display()
);
println!(
" Y range: {} to {}",
dims.min_y.display(),
dims.max_y.display()
);
}
}
"clearance" | "clear" => {
let pad_clear = minimum_pad_clearance(component);
let silk_clear = pad_to_silkscreen_clearance(component);
if output_json {
print_clearance_json(pad_clear.as_ref(), silk_clear.as_ref())?;
} else {
println!("Clearance Analysis for: {}", component.pattern);
println!("═══════════════════════════════════════════════════════════════");
if let Some(pc) = pad_clear {
println!("\nMinimum pad-to-pad clearance: {}", pc.clearance.display());
println!(" Location: {}", pc.location);
} else {
println!("\nNo pad-to-pad clearance (single pad or overlapping pads)");
}
if let Some(sc) = silk_clear {
println!("\nPad-to-silkscreen clearance: {}", sc.clearance.display());
println!(" Location: {}", sc.location);
} else {
println!("\nNo silkscreen elements found");
}
}
}
"pad" => {
let des = pad.ok_or("--pad required for pad measurement")?;
let info =
measure_pad(component, &des).ok_or_else(|| format!("Pad '{}' not found", des))?;
if output_json {
print_pad_json(&info)?;
} else {
println!("Pad {} info:", info.designator);
println!("═══════════════════════════════════════════════════════════════");
println!(" Position: ({}, {})", info.x.display(), info.y.display());
println!(
" Size: {} x {}",
info.width.display(),
info.height.display()
);
println!(" Shape: {}", info.shape);
if let Some(hole) = &info.hole {
println!(" Hole: {}", hole.display());
} else {
println!(" Type: SMD");
}
}
}
"pads" => {
let report = generate_report(component);
if output_json {
print_all_pads_json(&report.pads)?;
} else {
println!("All Pads for: {}", component.pattern);
println!("═══════════════════════════════════════════════════════════════");
println!(
"\n{:<6} {:>10} {:>10} {:>10} {:>10} {:>10} Shape",
"Pad", "X (mm)", "Y (mm)", "W (mm)", "H (mm)", "Hole"
);
println!(
"{:-<6} {:->10} {:->10} {:->10} {:->10} {:->10} {:-<12}",
"", "", "", "", "", "", ""
);
for pad_info in &report.pads {
let hole_str = pad_info
.hole
.as_ref()
.map(|h| format!("{:.3}", h.mm))
.unwrap_or_else(|| "-".to_string());
println!(
"{:<6} {:>10.3} {:>10.3} {:>10.3} {:>10.3} {:>10} {}",
pad_info.designator,
pad_info.x.mm,
pad_info.y.mm,
pad_info.width.mm,
pad_info.height.mm,
hole_str,
pad_info.shape
);
}
}
}
_ => {
return Err(format!(
"Unknown measurement type: '{}'. Use: all, distance, pitch, dimensions, clearance, pad, pads",
measure_type
).into());
}
}
Ok(())
}
fn print_measurement_report(report: &crate::footprint::MeasurementReport) {
println!("╔═══════════════════════════════════════════════════════════════╗");
println!("║ FOOTPRINT MEASUREMENT REPORT ║");
println!("╚═══════════════════════════════════════════════════════════════╝");
println!("\nFootprint: {}", report.name);
println!("\n┌─────────────────────────────────────────────────────────────────┐");
println!("│ DIMENSIONS │");
println!("└─────────────────────────────────────────────────────────────────┘");
println!(" Width: {}", report.dimensions.width.display());
println!(" Height: {}", report.dimensions.height.display());
if let Some(span) = &report.row_span {
println!(" Row span: {}", span.display());
}
println!("\n┌─────────────────────────────────────────────────────────────────┐");
println!(
"│ PADS ({} total) │",
report.pads.len()
);
println!("└─────────────────────────────────────────────────────────────────┘");
println!(
"\n{:<6} {:>10} {:>10} {:>10} {:>10} Shape",
"Pad", "X (mm)", "Y (mm)", "W (mm)", "H (mm)"
);
println!(
"{:-<6} {:->10} {:->10} {:->10} {:->10} {:-<12}",
"", "", "", "", "", ""
);
for pad in &report.pads {
println!(
"{:<6} {:>10.3} {:>10.3} {:>10.3} {:>10.3} {}",
pad.designator, pad.x.mm, pad.y.mm, pad.width.mm, pad.height.mm, pad.shape
);
}
if !report.pitch.is_empty() {
println!("\n┌─────────────────────────────────────────────────────────────────┐");
println!("│ PITCH ANALYSIS │");
println!("└─────────────────────────────────────────────────────────────────┘");
for pitch_info in &report.pitch {
println!(
"\n {} pitch: {}",
pitch_info.direction,
pitch_info.pitch.display()
);
println!(" {} adjacent pad pairs", pitch_info.count);
}
}
println!("\n┌─────────────────────────────────────────────────────────────────┐");
println!("│ CLEARANCES │");
println!("└─────────────────────────────────────────────────────────────────┘");
if let Some(pc) = &report.min_pad_clearance {
println!("\n Minimum pad-to-pad gap: {}", pc.clearance.display());
println!(" {}", pc.location);
}
if let Some(sc) = &report.silkscreen_clearance {
println!("\n Pad-to-silkscreen: {}", sc.clearance.display());
println!(" {}", sc.location);
}
}
fn print_measurement_report_json(
report: &crate::footprint::MeasurementReport,
) -> Result<(), Box<dyn std::error::Error>> {
#[derive(Serialize)]
struct MeasurementJson {
mm: f64,
mils: f64,
}
impl From<&Measurement> for MeasurementJson {
fn from(m: &Measurement) -> Self {
MeasurementJson {
mm: m.mm,
mils: m.mils,
}
}
}
#[derive(Serialize)]
struct PadInfoJson {
designator: String,
x_mm: f64,
y_mm: f64,
width_mm: f64,
height_mm: f64,
hole_mm: Option<f64>,
shape: String,
}
#[derive(Serialize)]
struct PitchJson {
pitch: MeasurementJson,
direction: String,
count: usize,
}
#[derive(Serialize)]
struct ClearanceJson {
feature1: String,
feature2: String,
clearance: MeasurementJson,
location: String,
}
#[derive(Serialize)]
struct ReportJson {
name: String,
dimensions: DimensionsJson,
pads: Vec<PadInfoJson>,
pitch: Vec<PitchJson>,
min_pad_clearance: Option<ClearanceJson>,
silkscreen_clearance: Option<ClearanceJson>,
row_span: Option<MeasurementJson>,
}
#[derive(Serialize)]
struct DimensionsJson {
width: MeasurementJson,
height: MeasurementJson,
min_x: MeasurementJson,
max_x: MeasurementJson,
min_y: MeasurementJson,
max_y: MeasurementJson,
}
let output = ReportJson {
name: report.name.clone(),
dimensions: DimensionsJson {
width: (&report.dimensions.width).into(),
height: (&report.dimensions.height).into(),
min_x: (&report.dimensions.min_x).into(),
max_x: (&report.dimensions.max_x).into(),
min_y: (&report.dimensions.min_y).into(),
max_y: (&report.dimensions.max_y).into(),
},
pads: report
.pads
.iter()
.map(|p| PadInfoJson {
designator: p.designator.clone(),
x_mm: p.x.mm,
y_mm: p.y.mm,
width_mm: p.width.mm,
height_mm: p.height.mm,
hole_mm: p.hole.as_ref().map(|h| h.mm),
shape: p.shape.clone(),
})
.collect(),
pitch: report
.pitch
.iter()
.map(|p| PitchJson {
pitch: (&p.pitch).into(),
direction: p.direction.clone(),
count: p.count,
})
.collect(),
min_pad_clearance: report.min_pad_clearance.as_ref().map(|c| ClearanceJson {
feature1: c.feature1.clone(),
feature2: c.feature2.clone(),
clearance: (&c.clearance).into(),
location: c.location.clone(),
}),
silkscreen_clearance: report.silkscreen_clearance.as_ref().map(|c| ClearanceJson {
feature1: c.feature1.clone(),
feature2: c.feature2.clone(),
clearance: (&c.clearance).into(),
location: c.location.clone(),
}),
row_span: report.row_span.as_ref().map(|s| s.into()),
};
let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
println!("{}", json);
Ok(())
}
fn print_distance_json(
dist: &crate::footprint::PadDistance,
) -> Result<(), Box<dyn std::error::Error>> {
#[derive(Serialize)]
struct DistanceJson {
pad1: String,
pad2: String,
center_to_center_mm: f64,
center_to_center_mils: f64,
edge_to_edge_mm: f64,
edge_to_edge_mils: f64,
}
let output = DistanceJson {
pad1: dist.pad1.clone(),
pad2: dist.pad2.clone(),
center_to_center_mm: dist.center_to_center.mm,
center_to_center_mils: dist.center_to_center.mils,
edge_to_edge_mm: dist.edge_to_edge.mm,
edge_to_edge_mils: dist.edge_to_edge.mils,
};
let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
println!("{}", json);
Ok(())
}
fn print_pitch_json(
pitches: &[crate::footprint::PitchAnalysis],
) -> Result<(), Box<dyn std::error::Error>> {
#[derive(Serialize)]
struct PitchJson {
direction: String,
pitch_mm: f64,
pitch_mils: f64,
pad_pair_count: usize,
}
let output: Vec<PitchJson> = pitches
.iter()
.map(|p| PitchJson {
direction: p.direction.clone(),
pitch_mm: p.pitch.mm,
pitch_mils: p.pitch.mils,
pad_pair_count: p.count,
})
.collect();
let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
println!("{}", json);
Ok(())
}
fn print_dimensions_json(
dims: &crate::footprint::FootprintDimensions,
) -> Result<(), Box<dyn std::error::Error>> {
#[derive(Serialize)]
struct DimsJson {
width_mm: f64,
width_mils: f64,
height_mm: f64,
height_mils: f64,
min_x_mm: f64,
max_x_mm: f64,
min_y_mm: f64,
max_y_mm: f64,
}
let output = DimsJson {
width_mm: dims.width.mm,
width_mils: dims.width.mils,
height_mm: dims.height.mm,
height_mils: dims.height.mils,
min_x_mm: dims.min_x.mm,
max_x_mm: dims.max_x.mm,
min_y_mm: dims.min_y.mm,
max_y_mm: dims.max_y.mm,
};
let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
println!("{}", json);
Ok(())
}
fn print_clearance_json(
pad_clear: Option<&crate::footprint::ClearanceResult>,
silk_clear: Option<&crate::footprint::ClearanceResult>,
) -> Result<(), Box<dyn std::error::Error>> {
#[derive(Serialize)]
struct ClearanceJson {
feature1: String,
feature2: String,
clearance_mm: f64,
clearance_mils: f64,
location: String,
}
#[derive(Serialize)]
struct Output {
pad_to_pad: Option<ClearanceJson>,
pad_to_silkscreen: Option<ClearanceJson>,
}
let output = Output {
pad_to_pad: pad_clear.map(|c| ClearanceJson {
feature1: c.feature1.clone(),
feature2: c.feature2.clone(),
clearance_mm: c.clearance.mm,
clearance_mils: c.clearance.mils,
location: c.location.clone(),
}),
pad_to_silkscreen: silk_clear.map(|c| ClearanceJson {
feature1: c.feature1.clone(),
feature2: c.feature2.clone(),
clearance_mm: c.clearance.mm,
clearance_mils: c.clearance.mils,
location: c.location.clone(),
}),
};
let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
println!("{}", json);
Ok(())
}
fn print_pad_json(info: &crate::footprint::PadInfo) -> Result<(), Box<dyn std::error::Error>> {
#[derive(Serialize)]
struct PadJson {
designator: String,
x_mm: f64,
y_mm: f64,
width_mm: f64,
height_mm: f64,
hole_mm: Option<f64>,
shape: String,
is_smd: bool,
}
let output = PadJson {
designator: info.designator.clone(),
x_mm: info.x.mm,
y_mm: info.y.mm,
width_mm: info.width.mm,
height_mm: info.height.mm,
hole_mm: info.hole.as_ref().map(|h| h.mm),
shape: info.shape.clone(),
is_smd: info.hole.is_none(),
};
let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
println!("{}", json);
Ok(())
}
fn print_all_pads_json(
pads: &[crate::footprint::PadInfo],
) -> Result<(), Box<dyn std::error::Error>> {
#[derive(Serialize)]
struct PadJson {
designator: String,
x_mm: f64,
y_mm: f64,
width_mm: f64,
height_mm: f64,
hole_mm: Option<f64>,
shape: String,
}
let output: Vec<PadJson> = pads
.iter()
.map(|p| PadJson {
designator: p.designator.clone(),
x_mm: p.x.mm,
y_mm: p.y.mm,
width_mm: p.width.mm,
height_mm: p.height.mm,
hole_mm: p.hole.as_ref().map(|h| h.mm),
shape: p.shape.clone(),
})
.collect();
let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
println!("{}", json);
Ok(())
}
const BLANK_PCBLIB_TEMPLATE: &[u8] = include_bytes!("../../data/blank/PcbLib1.PcbLib");
use crate::footprint::{ChipSpec, IpcDensity};
use crate::records::pcb::{PcbArc, PcbComponent, PcbFlags, PcbPrimitiveCommon, PcbTrack};
use crate::types::{Coord, CoordPoint};
pub fn cmd_create(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
if path.exists() {
return Err(format!("File already exists: {}", path.display()).into());
}
std::fs::write(path, BLANK_PCBLIB_TEMPLATE)
.map_err(|e| format!("Error creating file: {}", e))?;
println!("Created empty PcbLib: {}", path.display());
Ok(())
}
fn load_blank_pcblib() -> Result<PcbLib, Box<dyn std::error::Error>> {
Ok(PcbLib::open(Cursor::new(BLANK_PCBLIB_TEMPLATE))?)
}
pub fn cmd_add_footprint(
path: &Path,
name: &str,
description: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut lib = open_or_create_pcblib(path)?;
if lib.components.iter().any(|c| c.pattern == name) {
return Err(format!("Footprint '{}' already exists", name).into());
}
let mut det = ();
let mut component = PcbComponent::new_deterministic(name, &mut det);
if let Some(desc) = description {
component.set_description(desc);
}
lib.components.push(component);
save_pcblib(path, &lib)?;
println!("Added footprint '{}' to {}", name, path.display());
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_add_pad(
path: &Path,
footprint: &str,
designator: &str,
x: f64,
y: f64,
width: f64,
height: f64,
shape_str: &str,
hole: f64,
) -> Result<(), Box<dyn std::error::Error>> {
let mut lib = open_pcblib(path)?;
let component = lib
.components
.iter_mut()
.find(|c| c.pattern == footprint)
.ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
let shape = match shape_str.to_lowercase().as_str() {
"round" => PcbPadShape::Round,
"rectangular" | "rect" => PcbPadShape::Rectangular,
"rounded_rect" | "roundedrect" => PcbPadShape::RoundedRectangle,
"octagonal" | "oct" => PcbPadShape::Octagonal,
_ => return Err(format!("Unknown pad shape: {}", shape_str).into()),
};
let mut builder = FootprintBuilder::new(footprint);
if hole > 0.0 {
builder.add_th_pad(designator, x, y, width.max(height), hole, shape);
} else {
builder.add_smd_pad(designator, x, y, width, height, shape);
}
let mut det = ();
let temp = builder.build_deterministic(&mut det);
if let Some(PcbRecord::Pad(pad)) = temp.primitives.into_iter().next() {
component.add_primitive(PcbRecord::Pad(pad));
}
save_pcblib(path, &lib)?;
println!(
"Added pad '{}' to footprint '{}' at ({}, {}) mm",
designator, footprint, x, y
);
Ok(())
}
pub fn cmd_add_silkscreen(
path: &Path,
footprint: &str,
x1: f64,
y1: f64,
x2: f64,
y2: f64,
width: f64,
) -> Result<(), Box<dyn std::error::Error>> {
let mut lib = open_pcblib(path)?;
let component = lib
.components
.iter_mut()
.find(|c| c.pattern == footprint)
.ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
let track = PcbTrack {
common: PcbPrimitiveCommon {
layer: Layer::TOP_OVERLAY,
flags: PcbFlags::UNLOCKED | PcbFlags::UNKNOWN8,
unique_id: None,
},
start: CoordPoint::from_mms(x1, y1),
end: CoordPoint::from_mms(x2, y2),
width: Coord::from_mms(width),
unknown: vec![0u8; 16],
};
component.add_primitive(PcbRecord::Track(track));
save_pcblib(path, &lib)?;
println!("Added silkscreen line to footprint '{}'", footprint);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_add_arc(
path: &Path,
footprint: &str,
x: f64,
y: f64,
radius: f64,
start_angle: f64,
end_angle: f64,
width: f64,
) -> Result<(), Box<dyn std::error::Error>> {
let mut lib = open_pcblib(path)?;
let component = lib
.components
.iter_mut()
.find(|c| c.pattern == footprint)
.ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
let arc = PcbArc {
common: PcbPrimitiveCommon {
layer: Layer::TOP_OVERLAY,
flags: PcbFlags::UNLOCKED | PcbFlags::UNKNOWN8,
unique_id: None,
},
location: CoordPoint::from_mms(x, y),
radius: Coord::from_mms(radius),
start_angle,
end_angle,
width: Coord::from_mms(width),
};
component.add_primitive(PcbRecord::Arc(arc));
save_pcblib(path, &lib)?;
println!(
"Added silkscreen arc to footprint '{}' (center: ({}, {}) mm, radius: {} mm, {:.0}° to {:.0}°)",
footprint, x, y, radius, start_angle, end_angle
);
Ok(())
}
pub fn cmd_gen_chip(
path: &Path,
size: &str,
density_str: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let mut lib = open_or_create_pcblib(path)?;
let spec = match size.to_uppercase().as_str() {
"0201" => ChipSpec::chip_0201(),
"0402" => ChipSpec::chip_0402(),
"0603" => ChipSpec::chip_0603(),
"0805" => ChipSpec::chip_0805(),
"1206" => ChipSpec::chip_1206(),
_ => {
return Err(format!(
"Unknown chip size: {}. Supported: 0201, 0402, 0603, 0805, 1206",
size
)
.into());
}
};
let density = parse_density(density_str)?;
let mut det = ();
let component = spec.to_footprint(density).build_deterministic(&mut det);
let name = component.pattern.clone();
if lib.components.iter().any(|c| c.pattern == name) {
return Err(format!("Footprint '{}' already exists", name).into());
}
lib.components.push(component);
save_pcblib(path, &lib)?;
println!(
"Generated chip footprint '{}' with {} density",
name, density_str
);
Ok(())
}
fn parse_density(s: &str) -> Result<IpcDensity, Box<dyn std::error::Error>> {
match s.to_lowercase().as_str() {
"most" | "a" | "dense" => Ok(IpcDensity::MostDense),
"nominal" | "b" | "normal" => Ok(IpcDensity::Nominal),
"least" | "c" | "loose" => Ok(IpcDensity::LeastDense),
_ => Err(format!("Unknown density: {}. Use: most, nominal, least", s).into()),
}
}
pub fn cmd_render_svg(
path: &Path,
name: &str,
output: Option<PathBuf>,
scale: f64,
light: bool,
no_grid: bool,
no_designators: bool,
) -> Result<(), Box<dyn std::error::Error>> {
use std::fs;
let lib = open_pcblib(path)?;
let name_lower = name.to_lowercase();
let component = lib
.iter()
.find(|c| c.pattern.to_lowercase() == name_lower || matches_pattern(&c.pattern, name))
.ok_or_else(|| format!("Footprint '{}' not found", name))?;
let mut options = if light {
SvgOptions::light()
} else {
SvgOptions::default()
};
options.scale = scale;
options.show_grid = !no_grid;
options.show_designators = !no_designators;
let svg = render_svg(component, &options);
let output_path = output.unwrap_or_else(|| {
PathBuf::from(format!(
"{}.svg",
component.pattern.replace(['/', '\\', ' '], "_")
))
});
fs::write(&output_path, &svg).map_err(|e| format!("Error writing SVG: {}", e))?;
println!(
"Rendered footprint '{}' to {}",
component.pattern,
output_path.display()
);
println!(" Size: {} bytes", svg.len());
println!(" Theme: {}", if light { "light" } else { "dark" });
println!(" Scale: {} px/mil", scale);
Ok(())
}
pub fn cmd_render_png(
path: &Path,
name: &str,
output: Option<PathBuf>,
scale: f64,
target_width: Option<u32>,
) -> Result<(), Box<dyn std::error::Error>> {
use std::fs::File;
use std::io::BufWriter;
let lib = open_pcblib(path)?;
let name_lower = name.to_lowercase();
let component = lib
.iter()
.find(|c| c.pattern.to_lowercase() == name_lower || matches_pattern(&c.pattern, name))
.ok_or_else(|| format!("Footprint '{}' not found", name))?;
let options = SvgOptions {
scale,
show_grid: false, show_designators: true,
..Default::default()
};
let svg_data = render_svg(component, &options);
let tree = resvg::usvg::Tree::from_str(&svg_data, &resvg::usvg::Options::default())
.map_err(|e| format!("Error parsing SVG: {}", e))?;
let svg_size = tree.size();
let (width, height) = if let Some(w) = target_width {
let h = (w as f32 * svg_size.height() / svg_size.width()) as u32;
(w, h)
} else {
(svg_size.width() as u32, svg_size.height() as u32)
};
let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height)
.ok_or_else(|| "Failed to create pixmap".to_string())?;
pixmap.fill(resvg::tiny_skia::Color::from_rgba8(30, 30, 30, 255));
let scale_x = width as f32 / svg_size.width();
let scale_y = height as f32 / svg_size.height();
let transform = resvg::tiny_skia::Transform::from_scale(scale_x, scale_y);
resvg::render(&tree, transform, &mut pixmap.as_mut());
let output_path = output.unwrap_or_else(|| {
PathBuf::from(format!(
"{}.png",
component.pattern.replace(['/', '\\', ' '], "_")
))
});
let file = File::create(&output_path).map_err(|e| format!("Error creating file: {}", e))?;
let writer = BufWriter::new(file);
let mut encoder = png::Encoder::new(writer, width, height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut png_writer = encoder
.write_header()
.map_err(|e| format!("Error writing PNG header: {}", e))?;
png_writer
.write_image_data(pixmap.data())
.map_err(|e| format!("Error writing PNG data: {}", e))?;
println!(
"Rendered footprint '{}' to {}",
component.pattern,
output_path.display()
);
println!(" Size: {}x{} pixels", width, height);
Ok(())
}
pub fn cmd_render_ascii(
path: &Path,
name: &str,
max_width: usize,
max_height: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let lib = open_pcblib(path)?;
let name_lower = name.to_lowercase();
let component = lib
.iter()
.find(|c| c.pattern.to_lowercase() == name_lower || matches_pattern(&c.pattern, name))
.ok_or_else(|| format!("Footprint '{}' not found", name))?;
let options = AsciiOptions {
max_width,
max_height,
..Default::default()
};
let ascii = render_ascii(component, &options);
println!("{}", ascii);
Ok(())
}
fn open_or_create_pcblib(path: &Path) -> Result<PcbLib, Box<dyn std::error::Error>> {
if path.exists() {
open_pcblib(path)
} else {
load_blank_pcblib()
}
}
fn save_pcblib(path: &Path, lib: &PcbLib) -> Result<(), Box<dyn std::error::Error>> {
Ok(lib.save_to_file(path)?)
}
fn matches_pattern(text: &str, pattern: &str) -> bool {
let text = text.to_lowercase();
let pattern = pattern.to_lowercase();
fn matches(text: &[char], pattern: &[char]) -> bool {
match (text.first(), pattern.first()) {
(None, None) => true,
(None, Some('*')) => matches(text, &pattern[1..]),
(None, Some(_)) => false,
(Some(_), None) => false,
(Some(_), Some('*')) => {
matches(text, &pattern[1..]) || matches(&text[1..], pattern)
}
(Some(_), Some('?')) => {
matches(&text[1..], &pattern[1..])
}
(Some(t), Some(p)) => *t == *p && matches(&text[1..], &pattern[1..]),
}
}
let text_chars: Vec<char> = text.chars().collect();
let pattern_chars: Vec<char> = pattern.chars().collect();
matches(&text_chars, &pattern_chars)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PadJson {
pub designator: String,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
#[serde(default = "default_pad_shape")]
pub shape: String,
#[serde(default)]
pub hole: f64,
#[serde(default)]
pub rotation: f64,
}
fn default_pad_shape() -> String {
"rectangular".to_string()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LineJson {
pub x1: f64,
pub y1: f64,
pub x2: f64,
pub y2: f64,
#[serde(default = "default_line_width")]
pub width: f64,
}
fn default_line_width() -> f64 {
0.15
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ArcJson {
pub x: f64,
pub y: f64,
pub radius: f64,
pub start_angle: f64,
pub end_angle: f64,
#[serde(default = "default_line_width")]
pub width: f64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TextJson {
pub x: f64,
pub y: f64,
pub text: String,
#[serde(default = "default_text_height")]
pub height: f64,
#[serde(default)]
pub rotation: f64,
#[serde(default = "default_stroke_width")]
pub stroke_width: f64,
#[serde(default = "default_text_layer")]
pub layer: String,
#[serde(default)]
pub mirrored: bool,
}
fn default_text_height() -> f64 {
1.0
}
fn default_stroke_width() -> f64 {
0.15
}
fn default_text_layer() -> String {
"top_overlay".to_string()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PadRowJson {
pub count: usize,
pub pitch: String,
pub pad_width: String,
pub pad_height: String,
#[serde(default = "default_direction")]
pub direction: String,
#[serde(default = "default_start")]
pub start: u32,
#[serde(default)]
pub x: String,
#[serde(default)]
pub y: String,
#[serde(default = "default_pad_shape_str")]
pub shape: String,
#[serde(default)]
pub hole: String,
#[serde(default)]
pub use_spacing: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DualRowJson {
pub pads_per_side: usize,
pub pitch: String,
pub row_spacing: String,
#[serde(default)]
pub pad_width: Option<String>,
#[serde(default)]
pub pad_height: Option<String>,
#[serde(default)]
pub pad_diameter: Option<String>,
#[serde(default)]
pub hole: Option<String>,
#[serde(default = "default_pad_shape_str")]
pub shape: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct QuadPadsJson {
pub pads_per_side: usize,
pub pitch: String,
pub span: String,
pub pad_width: String,
pub pad_height: String,
#[serde(default = "default_pad_shape_str")]
pub shape: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PadGridJson {
pub rows: usize,
pub cols: usize,
pub pitch: String,
pub pad_diameter: String,
#[serde(default = "default_round_shape")]
pub shape: String,
#[serde(default)]
pub skip_center: String,
}
fn default_direction() -> String {
"horizontal".to_string()
}
fn default_start() -> u32 {
1
}
fn default_pad_shape_str() -> String {
"rectangular".to_string()
}
fn default_round_shape() -> String {
"round".to_string()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FootprintJson {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub pads: Vec<PadJson>,
#[serde(default)]
pub lines: Vec<LineJson>,
#[serde(default)]
pub arcs: Vec<ArcJson>,
#[serde(default)]
pub texts: Vec<TextJson>,
#[serde(default)]
pub pad_rows: Vec<PadRowJson>,
#[serde(default)]
pub dual_rows: Vec<DualRowJson>,
#[serde(default)]
pub quad_pads: Vec<QuadPadsJson>,
#[serde(default)]
pub pad_grids: Vec<PadGridJson>,
}
pub fn cmd_add_json(
path: &Path,
json_file: Option<String>,
json_str: Option<String>,
) -> Result<(), 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)
.map_err(|e| format!("Error reading from stdin: {}", e))?;
buffer
}
(Some(ref file_path), None) => std::fs::read_to_string(file_path)
.map_err(|e| format!("Error reading file '{}': {}", file_path, e))?,
(None, None) => {
return Err("Must provide either --file <path> or --json <string>"
.to_string()
.into());
}
};
let footprint_def: FootprintJson =
serde_json::from_str(&json_content).map_err(|e| format!("Invalid JSON: {}", e))?;
let mut lib = open_or_create_pcblib(path)?;
if lib
.components
.iter()
.any(|c| c.pattern == footprint_def.name)
{
return Err(format!("Footprint '{}' already exists", footprint_def.name).into());
}
let mut builder = FootprintBuilder::new(&footprint_def.name);
if !footprint_def.description.is_empty() {
builder = builder.description(&footprint_def.description);
}
for row in &footprint_def.pad_rows {
let pitch_mm = parse_unit_value_or_mm(&row.pitch)?;
let pad_width_mm = parse_unit_value_or_mm(&row.pad_width)?;
let pad_height_mm = parse_unit_value_or_mm(&row.pad_height)?;
let x_mm = if row.x.is_empty() {
0.0
} else {
parse_unit_value_or_mm(&row.x)?
};
let y_mm = if row.y.is_empty() {
0.0
} else {
parse_unit_value_or_mm(&row.y)?
};
let hole_mm = if row.hole.is_empty() {
0.0
} else {
parse_unit_value_or_mm(&row.hole)?
};
let dir = PadRowDirection::try_parse(&row.direction)
.ok_or_else(|| format!("Invalid direction '{}' in pad_row", row.direction))?;
let shape = parse_pad_shape(&row.shape)?;
if hole_mm > 0.0 {
let pad_diameter = pad_width_mm.max(pad_height_mm);
if row.use_spacing {
let pad_along_row = match dir {
PadRowDirection::Horizontal => pad_width_mm,
PadRowDirection::Vertical => pad_height_mm,
};
let effective_pitch = pitch_mm + pad_along_row;
builder.add_th_pad_row(
row.count,
effective_pitch,
pad_diameter,
hole_mm,
x_mm,
y_mm,
dir,
row.start,
shape,
);
} else {
builder.add_th_pad_row(
row.count,
pitch_mm,
pad_diameter,
hole_mm,
x_mm,
y_mm,
dir,
row.start,
shape,
);
}
} else if row.use_spacing {
builder.add_pad_row_with_spacing(
row.count,
pitch_mm,
pad_width_mm,
pad_height_mm,
x_mm,
y_mm,
dir,
row.start,
shape,
);
} else {
builder.add_pad_row(
row.count,
pitch_mm,
pad_width_mm,
pad_height_mm,
x_mm,
y_mm,
dir,
row.start,
shape,
);
}
}
for dual in &footprint_def.dual_rows {
let pitch_mm = parse_unit_value_or_mm(&dual.pitch)?;
let row_spacing_mm = parse_unit_value_or_mm(&dual.row_spacing)?;
let shape = parse_pad_shape(&dual.shape)?;
if let Some(ref hole_str) = dual.hole {
let hole_mm = parse_unit_value_or_mm(hole_str)?;
let pad_dia_mm = if let Some(ref d) = dual.pad_diameter {
parse_unit_value_or_mm(d)?
} else if let Some(ref w) = dual.pad_width {
parse_unit_value_or_mm(w)?
} else {
return Err("Through-hole dual_row requires pad_diameter or pad_width"
.to_string()
.into());
};
builder.add_dual_row_th(
dual.pads_per_side,
pitch_mm,
row_spacing_mm,
pad_dia_mm,
hole_mm,
shape,
);
} else {
let pad_width_mm = dual
.pad_width
.as_ref()
.ok_or("SMD dual_row requires pad_width")?;
let pad_height_mm = dual
.pad_height
.as_ref()
.ok_or("SMD dual_row requires pad_height")?;
let pad_width_mm = parse_unit_value_or_mm(pad_width_mm)?;
let pad_height_mm = parse_unit_value_or_mm(pad_height_mm)?;
builder.add_dual_row_smd(
dual.pads_per_side,
pitch_mm,
row_spacing_mm,
pad_width_mm,
pad_height_mm,
shape,
);
}
}
for quad in &footprint_def.quad_pads {
let pitch_mm = parse_unit_value_or_mm(&quad.pitch)?;
let span_mm = parse_unit_value_or_mm(&quad.span)?;
let pad_width_mm = parse_unit_value_or_mm(&quad.pad_width)?;
let pad_height_mm = parse_unit_value_or_mm(&quad.pad_height)?;
let shape = parse_pad_shape(&quad.shape)?;
builder.add_quad_pads_smd(
quad.pads_per_side,
pitch_mm,
span_mm,
pad_width_mm,
pad_height_mm,
shape,
);
}
for grid in &footprint_def.pad_grids {
let pitch_mm = parse_unit_value_or_mm(&grid.pitch)?;
let pad_diameter_mm = parse_unit_value_or_mm(&grid.pad_diameter)?;
let skip_center_mm = if grid.skip_center.is_empty() {
0.0
} else {
parse_unit_value_or_mm(&grid.skip_center)?
};
let shape = parse_pad_shape(&grid.shape)?;
builder.add_pad_grid(
grid.rows,
grid.cols,
pitch_mm,
pad_diameter_mm,
shape,
skip_center_mm,
);
}
for pad in &footprint_def.pads {
let shape = parse_pad_shape(&pad.shape)?;
if pad.hole > 0.0 {
builder.add_th_pad(
&pad.designator,
pad.x,
pad.y,
pad.width.max(pad.height),
pad.hole,
shape,
);
} else {
builder.add_smd_pad(&pad.designator, pad.x, pad.y, pad.width, pad.height, shape);
}
}
for line in &footprint_def.lines {
builder.add_silkscreen_line(line.x1, line.y1, line.x2, line.y2, line.width);
}
for arc in &footprint_def.arcs {
builder.add_silkscreen_arc(
arc.x,
arc.y,
arc.radius,
arc.start_angle,
arc.end_angle,
arc.width,
);
}
let mut det = ();
let mut component = builder.build_deterministic(&mut det);
for text_def in &footprint_def.texts {
let layer = parse_pcb_layer(&text_def.layer)?;
let text = PcbText::new(
text_def.x,
text_def.y,
&text_def.text,
text_def.height,
text_def.stroke_width,
text_def.rotation,
text_def.mirrored,
layer,
);
component.add_primitive(PcbRecord::Text(text));
}
let pad_count = component.pad_count();
let line_count = footprint_def.lines.len();
let arc_count = footprint_def.arcs.len();
let text_count = footprint_def.texts.len();
lib.components.push(component);
save_pcblib(path, &lib)?;
let mut parts = vec![format!("{} pads", pad_count)];
if line_count > 0 {
parts.push(format!("{} lines", line_count));
}
if arc_count > 0 {
parts.push(format!("{} arcs", arc_count));
}
if text_count > 0 {
parts.push(format!("{} texts", text_count));
}
println!(
"Added footprint '{}' with {} to {}",
footprint_def.name,
parts.join(", "),
path.display()
);
Ok(())
}
fn parse_pcb_layer(s: &str) -> Result<Layer, String> {
match s.to_lowercase().replace('_', "").as_str() {
"topoverlay" | "silkscreen" | "top_overlay" => Ok(Layer::TOP_OVERLAY),
"bottomoverlay" | "bottom_overlay" => Ok(Layer::BOTTOM_OVERLAY),
"top" | "toplayer" => Ok(Layer::TOP_LAYER),
"bottom" | "bottomlayer" => Ok(Layer::BOTTOM_LAYER),
_ => Err(format!(
"Unknown layer: {}. Use: top_overlay, bottom_overlay, top, bottom",
s
)),
}
}
fn parse_pad_shape(s: &str) -> Result<PcbPadShape, String> {
match s.to_lowercase().as_str() {
"round" => Ok(PcbPadShape::Round),
"rectangular" | "rect" => Ok(PcbPadShape::Rectangular),
"rounded_rect" | "roundedrect" | "rounded_rectangle" => Ok(PcbPadShape::RoundedRectangle),
"octagonal" | "oct" => Ok(PcbPadShape::Octagonal),
_ => Err(format!(
"Unknown pad shape: {}. Use: round, rectangular, rounded_rect, octagonal",
s
)),
}
}
fn parse_unit_value(s: &str) -> Result<f64, String> {
let (coord, _unit) =
Unit::parse_with_unit(s).map_err(|e| format!("Invalid value '{}': {:?}", s, e))?;
Ok(coord.to_mms())
}
fn parse_unit_value_or_mm(s: &str) -> Result<f64, String> {
let s = s.trim();
if let Ok((coord, _unit)) = Unit::parse_with_unit(s) {
return Ok(coord.to_mms());
}
s.parse::<f64>().map_err(|_| {
format!(
"Invalid value '{}': expected number with optional unit (e.g., '0.5mm', '50mil')",
s
)
})
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_add_pad_row(
path: &Path,
footprint: &str,
count: usize,
pitch: &str,
pad_width: &str,
pad_height: &str,
direction: &str,
start: u32,
x: &str,
y: &str,
shape_str: &str,
hole: &str,
use_spacing: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let mut lib = open_pcblib(path)?;
let component = lib
.components
.iter_mut()
.find(|c| c.pattern == footprint)
.ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
let pitch_mm = parse_unit_value(pitch)?;
let pad_width_mm = parse_unit_value(pad_width)?;
let pad_height_mm = parse_unit_value(pad_height)?;
let x_mm = parse_unit_value(x)?;
let y_mm = parse_unit_value(y)?;
let hole_mm = parse_unit_value(hole)?;
let dir = PadRowDirection::try_parse(direction).ok_or_else(|| {
format!(
"Invalid direction '{}'. Use: horizontal (h/x) or vertical (v/y)",
direction
)
})?;
let shape = parse_pad_shape(shape_str)?;
let mut builder = FootprintBuilder::new(footprint);
if hole_mm > 0.0 {
let pad_diameter = pad_width_mm.max(pad_height_mm);
if use_spacing {
let pad_along_row = match dir {
PadRowDirection::Horizontal => pad_width_mm,
PadRowDirection::Vertical => pad_height_mm,
};
let effective_pitch = pitch_mm + pad_along_row;
builder.add_th_pad_row(
count,
effective_pitch,
pad_diameter,
hole_mm,
x_mm,
y_mm,
dir,
start,
shape,
);
} else {
builder.add_th_pad_row(
count,
pitch_mm,
pad_diameter,
hole_mm,
x_mm,
y_mm,
dir,
start,
shape,
);
}
} else {
if use_spacing {
builder.add_pad_row_with_spacing(
count,
pitch_mm,
pad_width_mm,
pad_height_mm,
x_mm,
y_mm,
dir,
start,
shape,
);
} else {
builder.add_pad_row(
count,
pitch_mm,
pad_width_mm,
pad_height_mm,
x_mm,
y_mm,
dir,
start,
shape,
);
}
}
let mut det = ();
let temp = builder.build_deterministic(&mut det);
for prim in temp.primitives {
component.add_primitive(prim);
}
save_pcblib(path, &lib)?;
let term = if use_spacing { "spacing" } else { "pitch" };
println!(
"Added {} pads to '{}' ({} {} {}, direction: {})",
count,
footprint,
pitch,
term,
if hole_mm > 0.0 { "through-hole" } else { "SMD" },
direction
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_add_dual_row(
path: &Path,
footprint: &str,
pads_per_side: usize,
pitch: &str,
row_spacing: &str,
pad_width: Option<&str>,
pad_height: Option<&str>,
pad_diameter: Option<&str>,
hole: Option<&str>,
shape_str: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let mut lib = open_pcblib(path)?;
let component = lib
.components
.iter_mut()
.find(|c| c.pattern == footprint)
.ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
let pitch_mm = parse_unit_value(pitch)?;
let row_spacing_mm = parse_unit_value(row_spacing)?;
let shape = parse_pad_shape(shape_str)?;
let mut builder = FootprintBuilder::new(footprint);
if let Some(hole_str) = hole {
let hole_mm = parse_unit_value(hole_str)?;
let pad_dia_mm = if let Some(d) = pad_diameter {
parse_unit_value(d)?
} else if let Some(w) = pad_width {
parse_unit_value(w)?
} else {
return Err("Through-hole pads require --pad-diameter or --pad-width"
.to_string()
.into());
};
builder.add_dual_row_th(
pads_per_side,
pitch_mm,
row_spacing_mm,
pad_dia_mm,
hole_mm,
shape,
);
} else {
let pad_w = pad_width.ok_or("SMD pads require --pad-width")?;
let pad_h = pad_height.ok_or("SMD pads require --pad-height")?;
let pad_width_mm = parse_unit_value(pad_w)?;
let pad_height_mm = parse_unit_value(pad_h)?;
builder.add_dual_row_smd(
pads_per_side,
pitch_mm,
row_spacing_mm,
pad_width_mm,
pad_height_mm,
shape,
);
}
let mut det = ();
let temp = builder.build_deterministic(&mut det);
for prim in temp.primitives {
component.add_primitive(prim);
}
save_pcblib(path, &lib)?;
let total_pads = pads_per_side * 2;
let pad_type = if hole.is_some() {
"through-hole"
} else {
"SMD"
};
println!(
"Added dual row ({} {} pads, {} per side) to '{}' (pitch: {}, row spacing: {})",
total_pads, pad_type, pads_per_side, footprint, pitch, row_spacing
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_add_quad_pads(
path: &Path,
footprint: &str,
pads_per_side: usize,
pitch: &str,
span: &str,
pad_width: &str,
pad_height: &str,
shape_str: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let mut lib = open_pcblib(path)?;
let component = lib
.components
.iter_mut()
.find(|c| c.pattern == footprint)
.ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
let pitch_mm = parse_unit_value(pitch)?;
let span_mm = parse_unit_value(span)?;
let pad_width_mm = parse_unit_value(pad_width)?;
let pad_height_mm = parse_unit_value(pad_height)?;
let shape = parse_pad_shape(shape_str)?;
let mut builder = FootprintBuilder::new(footprint);
builder.add_quad_pads_smd(
pads_per_side,
pitch_mm,
span_mm,
pad_width_mm,
pad_height_mm,
shape,
);
let mut det = ();
let temp = builder.build_deterministic(&mut det);
for prim in temp.primitives {
component.add_primitive(prim);
}
save_pcblib(path, &lib)?;
let total_pads = pads_per_side * 4;
println!(
"Added quad arrangement ({} SMD pads, {} per side) to '{}' (pitch: {}, span: {})",
total_pads, pads_per_side, footprint, pitch, span
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_add_pad_grid(
path: &Path,
footprint: &str,
rows: usize,
cols: usize,
pitch: &str,
pad_diameter: &str,
shape_str: &str,
skip_center: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let mut lib = open_pcblib(path)?;
let component = lib
.components
.iter_mut()
.find(|c| c.pattern == footprint)
.ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
let pitch_mm = parse_unit_value(pitch)?;
let pad_diameter_mm = parse_unit_value(pad_diameter)?;
let skip_center_mm = parse_unit_value(skip_center)?;
let shape = parse_pad_shape(shape_str)?;
let mut builder = FootprintBuilder::new(footprint);
builder.add_pad_grid(rows, cols, pitch_mm, pad_diameter_mm, shape, skip_center_mm);
let mut det = ();
let temp = builder.build_deterministic(&mut det);
let pad_count = temp.primitives.len();
for prim in temp.primitives {
component.add_primitive(prim);
}
save_pcblib(path, &lib)?;
let max_pads = rows * cols;
let skipped = max_pads - pad_count;
if skipped > 0 {
println!(
"Added {}x{} grid ({} pads, {} skipped in center) to '{}' (pitch: {})",
rows, cols, pad_count, skipped, footprint, pitch
);
} else {
println!(
"Added {}x{} grid ({} pads) to '{}' (pitch: {})",
rows, cols, pad_count, footprint, pitch
);
}
Ok(())
}