1use std::collections::HashMap;
11use std::fs::File;
12use std::io::{BufReader, Cursor};
13use std::path::{Path, PathBuf};
14
15use png;
16use resvg;
17use serde::{Deserialize, Serialize};
18use serde_json;
19
20use crate::dump::fmt_coord_val;
21use crate::footprint::{
22 AsciiOptions, FootprintBuilder, PadRowDirection, SvgOptions, render_ascii, render_svg,
23};
24use crate::io::PcbLib;
25use crate::records::pcb::{PcbPad, PcbPadShape, PcbRecord, PcbText};
26use crate::types::{Layer, Unit};
27
28use super::util::alphanumeric_sort;
29use crate::ops::output::*;
30
31fn open_pcblib(path: &Path) -> Result<PcbLib, Box<dyn std::error::Error>> {
32 let file = File::open(path)?;
33 Ok(PcbLib::open(BufReader::new(file))?)
34}
35
36fn categorize_footprint(pattern: &str, description: &str) -> &'static str {
38 let pattern_lower = pattern.to_lowercase();
39 let desc_lower = description.to_lowercase();
40
41 if pattern_lower.contains("qfp")
43 || pattern_lower.contains("tqfp")
44 || pattern_lower.contains("lqfp")
45 {
46 return "QFP";
47 }
48 if pattern_lower.contains("qfn")
49 || pattern_lower.contains("dfn")
50 || pattern_lower.contains("mlf")
51 {
52 return "QFN/DFN";
53 }
54 if pattern_lower.contains("bga")
55 || pattern_lower.contains("csbga")
56 || pattern_lower.contains("wlcsp")
57 {
58 return "BGA";
59 }
60 if pattern_lower.contains("soic")
61 || pattern_lower.contains("so-")
62 || pattern_lower.contains("sop")
63 {
64 return "SOIC/SOP";
65 }
66 if pattern_lower.contains("ssop")
67 || pattern_lower.contains("tssop")
68 || pattern_lower.contains("msop")
69 {
70 return "SSOP/TSSOP";
71 }
72 if pattern_lower.contains("sot") {
73 return "SOT";
74 }
75 if pattern_lower.contains("dip") || pattern_lower.contains("pdip") {
76 return "DIP";
77 }
78 if pattern_lower.contains("to-")
79 || pattern_lower.contains("to2")
80 || pattern_lower.contains("to3")
81 || pattern_lower.contains("dpak")
82 || pattern_lower.contains("d2pak")
83 {
84 return "TO/DPAK";
85 }
86
87 if pattern_lower.starts_with("0402")
89 || pattern_lower.starts_with("0603")
90 || pattern_lower.starts_with("0805")
91 || pattern_lower.starts_with("1206")
92 || pattern_lower.starts_with("1210")
93 || pattern_lower.starts_with("0201")
94 || pattern_lower.starts_with("1812")
95 || pattern_lower.starts_with("2010")
96 || pattern_lower.starts_with("2512")
97 {
98 return "Chip (SMD)";
99 }
100 if pattern_lower.contains("cap") || desc_lower.contains("capacitor") {
101 return "Capacitor";
102 }
103 if pattern_lower.contains("res") || desc_lower.contains("resistor") {
104 return "Resistor";
105 }
106 if pattern_lower.contains("ind")
107 || pattern_lower.contains("ferrite")
108 || desc_lower.contains("inductor")
109 {
110 return "Inductor";
111 }
112
113 if pattern_lower.contains("header")
115 || pattern_lower.contains("conn")
116 || pattern_lower.contains("socket")
117 || pattern_lower.contains("pin")
118 || pattern_lower.contains("terminal")
119 {
120 return "Connector";
121 }
122 if pattern_lower.contains("usb") {
123 return "USB";
124 }
125 if pattern_lower.contains("rj45") || pattern_lower.contains("ethernet") {
126 return "RJ45/Ethernet";
127 }
128
129 if pattern_lower.contains("diode")
131 || pattern_lower.contains("sod")
132 || pattern_lower.contains("sma")
133 || pattern_lower.contains("smb")
134 || pattern_lower.contains("smc")
135 {
136 return "Diode";
137 }
138 if pattern_lower.contains("led") {
139 return "LED";
140 }
141
142 if pattern_lower.contains("crystal")
144 || pattern_lower.contains("xtal")
145 || pattern_lower.contains("osc")
146 {
147 return "Crystal/Oscillator";
148 }
149
150 if pattern_lower.contains("test") || pattern_lower.contains("tp") {
152 return "Test Point";
153 }
154
155 if pattern_lower.contains("th")
157 || pattern_lower.contains("axial")
158 || pattern_lower.contains("radial")
159 {
160 return "Through-Hole";
161 }
162
163 "Other"
164}
165
166fn pad_shape_name(shape: PcbPadShape) -> &'static str {
168 shape.name()
169}
170
171fn record_type_name(record: &PcbRecord) -> &'static str {
173 match record {
174 PcbRecord::Arc(_) => "Arc",
175 PcbRecord::Pad(_) => "Pad",
176 PcbRecord::Via(_) => "Via",
177 PcbRecord::Track(_) => "Track",
178 PcbRecord::Text(_) => "Text",
179 PcbRecord::Fill(_) => "Fill",
180 PcbRecord::Region(_) => "Region",
181 PcbRecord::ComponentBody(_) => "ComponentBody",
182 PcbRecord::Polygon(_) => "Polygon",
183 PcbRecord::Unknown { .. } => "Unknown",
184 }
185}
186
187fn layer_name(layer: &Layer) -> String {
189 match layer.to_byte() {
190 1 => "Top".to_string(),
191 32 => "Bottom".to_string(),
192 74 => "Multi".to_string(),
193 _ => format!("L{}", layer.to_byte()),
194 }
195}
196
197pub fn cmd_overview(path: &Path) -> Result<PcbLibOverview, Box<dyn std::error::Error>> {
203 let lib = open_pcblib(path)?;
204
205 let mut categories: HashMap<&'static str, Vec<FootprintSummaryExt>> = HashMap::new();
209
210 for comp in lib.iter() {
211 let category = categorize_footprint(&comp.pattern, &comp.description);
212 categories
213 .entry(category)
214 .or_default()
215 .push(FootprintSummaryExt {
216 name: comp.pattern.clone(),
217 description: comp.description.clone(),
218 pad_count: comp.pad_count(),
219 });
220 }
221
222 let category_order = [
224 "QFP",
225 "QFN/DFN",
226 "BGA",
227 "SOIC/SOP",
228 "SSOP/TSSOP",
229 "SOT",
230 "DIP",
231 "TO/DPAK",
232 "Chip (SMD)",
233 "Capacitor",
234 "Resistor",
235 "Inductor",
236 "Connector",
237 "USB",
238 "RJ45/Ethernet",
239 "Diode",
240 "LED",
241 "Crystal/Oscillator",
242 "Test Point",
243 "Through-Hole",
244 "Other",
245 ];
246
247 let mut footprints_by_category = Vec::new();
248 for category in category_order.iter() {
249 if let Some(footprints) = categories.remove(*category) {
250 footprints_by_category.push((category.to_string(), footprints));
251 }
252 }
253
254 for (category, footprints) in categories {
256 if !footprints.is_empty() {
257 footprints_by_category.push((category.to_string(), footprints));
258 }
259 }
260
261 let mut total_pads = 0;
265 let mut smd_pads = 0;
266 let mut th_pads = 0;
267 let mut pad_shapes: HashMap<&'static str, usize> = HashMap::new();
268
269 for comp in lib.iter() {
270 for pad in comp.pads() {
271 total_pads += 1;
272 if pad.has_hole() {
273 th_pads += 1;
274 } else {
275 smd_pads += 1;
276 }
277 *pad_shapes
278 .entry(pad_shape_name(pad.shape_top()))
279 .or_insert(0) += 1;
280 }
281 }
282
283 let mut pad_shapes_vec: Vec<_> = pad_shapes
284 .into_iter()
285 .map(|(k, v)| (k.to_string(), v))
286 .collect();
287 pad_shapes_vec.sort_by(|a, b| b.1.cmp(&a.1));
288
289 let mut hole_sizes: HashMap<String, usize> = HashMap::new();
293 for comp in lib.iter() {
294 for pad in comp.pads() {
295 if pad.has_hole() && pad.hole_size.to_raw() > 0 {
296 let size_str = fmt_coord_val(&pad.hole_size);
297 *hole_sizes.entry(size_str).or_insert(0) += 1;
298 }
299 }
300 }
301
302 let mut hole_sizes_vec: Vec<_> = hole_sizes.into_iter().collect();
303 hole_sizes_vec.sort_by(|a, b| b.1.cmp(&a.1));
304
305 let mut by_pads: Vec<_> = lib.iter().collect();
309 by_pads.sort_by_key(|b| std::cmp::Reverse(b.pad_count()));
310
311 let largest_footprints = by_pads
312 .iter()
313 .take(10)
314 .map(|comp| FootprintSummaryExt {
315 name: comp.pattern.clone(),
316 description: comp.description.clone(),
317 pad_count: comp.pad_count(),
318 })
319 .collect();
320
321 Ok(PcbLibOverview {
322 path: path.display().to_string(),
323 total_footprints: lib.components.len(),
324 unique_id: lib.unique_id.clone(),
325 footprints_by_category,
326 pad_statistics: PadStatistics {
327 total_pads,
328 smd_pads,
329 th_pads,
330 pad_shapes: pad_shapes_vec,
331 },
332 hole_sizes: hole_sizes_vec.into_iter().take(10).collect(),
333 largest_footprints,
334 })
335}
336
337pub fn cmd_list(path: &Path) -> Result<PcbLibFootprintList, Box<dyn std::error::Error>> {
339 let lib = open_pcblib(path)?;
340
341 let footprints = lib
342 .iter()
343 .map(|comp| FootprintSummaryExt {
344 name: comp.pattern.clone(),
345 description: comp.description.clone(),
346 pad_count: comp.pad_count(),
347 })
348 .collect();
349
350 Ok(PcbLibFootprintList {
351 path: path.display().to_string(),
352 total_footprints: lib.components.len(),
353 footprints,
354 })
355}
356
357pub fn cmd_search(
359 path: &Path,
360 query: &str,
361) -> Result<PcbLibSearchResults, Box<dyn std::error::Error>> {
362 let lib = open_pcblib(path)?;
363
364 let query_lower = query.to_lowercase();
365 let has_wildcard = query.contains('*');
366
367 let matches: Vec<_> = lib
368 .iter()
369 .filter(|comp| {
370 let name = comp.pattern.to_lowercase();
371 let desc = comp.description.to_lowercase();
372
373 if has_wildcard {
374 let pattern = query_lower.replace('*', "");
375 name.contains(&pattern) || desc.contains(&pattern)
376 } else {
377 name.contains(&query_lower) || desc.contains(&query_lower)
378 }
379 })
380 .map(|comp| FootprintSummaryExt {
381 name: comp.pattern.clone(),
382 description: comp.description.clone(),
383 pad_count: comp.pad_count(),
384 })
385 .collect();
386
387 Ok(PcbLibSearchResults {
388 query: query.to_string(),
389 total_matches: matches.len(),
390 results: matches,
391 })
392}
393
394pub fn cmd_info(path: &Path) -> Result<PcbLibInfo, Box<dyn std::error::Error>> {
400 let lib = open_pcblib(path)?;
401
402 let mut primitive_counts: HashMap<&'static str, usize> = HashMap::new();
404 let mut total_primitives = 0;
405
406 for comp in lib.iter() {
407 for prim in &comp.primitives {
408 let name = record_type_name(prim);
409 *primitive_counts.entry(name).or_insert(0) += 1;
410 total_primitives += 1;
411 }
412 }
413
414 let mut primitive_types: Vec<_> = primitive_counts
415 .into_iter()
416 .map(|(k, v)| (k.to_string(), v))
417 .collect();
418 primitive_types.sort_by(|a, b| b.1.cmp(&a.1));
419
420 Ok(PcbLibInfo {
421 path: path.display().to_string(),
422 footprint_count: lib.components.len(),
423 unique_id: lib.unique_id.clone(),
424 total_primitives,
425 primitive_types,
426 })
427}
428
429pub fn cmd_footprint(
431 path: &Path,
432 name: &str,
433 show_primitives: bool,
434) -> Result<PcbLibFootprintDetail, Box<dyn std::error::Error>> {
435 let lib = open_pcblib(path)?;
436
437 let name_lower = name.to_lowercase();
438 let comp = lib
439 .iter()
440 .find(|c| c.pattern.to_lowercase() == name_lower)
441 .ok_or_else(|| format!("Footprint '{}' not found", name))?;
442
443 let bounds = comp.calculate_bounds();
445
446 let mut pads: Vec<&PcbPad> = comp.pads().collect();
448 pads.sort_by(|a, b| alphanumeric_sort(&a.designator, &b.designator));
449
450 let pad_details = pads
451 .iter()
452 .map(|pad| {
453 let size = pad.size_top();
454 let size_str = format!("{}x{}", fmt_coord_val(&size.x), fmt_coord_val(&size.y));
455 let hole_str = if pad.has_hole() {
456 Some(fmt_coord_val(&pad.hole_size))
457 } else {
458 None
459 };
460 PadDetail {
461 designator: pad.designator.clone(),
462 shape: pad_shape_name(pad.shape_top()).to_string(),
463 size: size_str,
464 hole: hole_str,
465 layer: layer_name(&pad.common.layer),
466 }
467 })
468 .collect();
469
470 let primitive_counts = if show_primitives {
471 let mut prim_counts: HashMap<&'static str, usize> = HashMap::new();
472 for prim in &comp.primitives {
473 *prim_counts.entry(record_type_name(prim)).or_insert(0) += 1;
474 }
475 let mut counts: Vec<_> = prim_counts
476 .into_iter()
477 .map(|(k, v)| (k.to_string(), v))
478 .collect();
479 counts.sort_by(|a, b| b.1.cmp(&a.1));
480 Some(counts)
481 } else {
482 None
483 };
484
485 Ok(PcbLibFootprintDetail {
486 pattern: comp.pattern.clone(),
487 description: comp.description.clone(),
488 height: if comp.height.to_raw() > 0 {
489 fmt_coord_val(&comp.height)
490 } else {
491 String::new()
492 },
493 pad_count: comp.pad_count(),
494 total_primitives: comp.primitive_count(),
495 bounding_box: BoundingBox {
496 width: fmt_coord_val(&bounds.width()),
497 height: fmt_coord_val(&bounds.height()),
498 },
499 pads: pad_details,
500 primitive_counts,
501 })
502}
503
504pub fn cmd_pads(
506 path: &Path,
507 footprint_filter: Option<String>,
508 by_shape: bool,
509) -> Result<PcbLibPadList, Box<dyn std::error::Error>> {
510 let lib = open_pcblib(path)?;
511
512 let filter_lower = footprint_filter.as_ref().map(|s| s.to_lowercase());
513
514 let mut all_pads: Vec<PadWithFootprint> = Vec::new();
515
516 for comp in lib.iter() {
517 if let Some(ref filter) = filter_lower {
518 if !comp.pattern.to_lowercase().contains(filter) {
519 continue;
520 }
521 }
522
523 for pad in comp.pads() {
524 let size = pad.size_top();
525 let size_str = format!("{}x{}", fmt_coord_val(&size.x), fmt_coord_val(&size.y));
526 let hole_str = if pad.has_hole() {
527 Some(fmt_coord_val(&pad.hole_size))
528 } else {
529 None
530 };
531 all_pads.push(PadWithFootprint {
532 footprint_name: comp.pattern.clone(),
533 designator: pad.designator.clone(),
534 size: size_str,
535 hole: hole_str,
536 shape: pad_shape_name(pad.shape_top()).to_string(),
537 });
538 }
539 }
540
541 let pads_by_shape = if by_shape {
542 let mut by_shape: HashMap<String, Vec<PadWithFootprint>> = HashMap::new();
543 for pad in &all_pads {
544 by_shape
545 .entry(pad.shape.clone())
546 .or_default()
547 .push(pad.clone());
548 }
549
550 let shape_order = ["Round", "Rectangular", "Rounded Rect", "Octagonal"];
551 let mut result = Vec::new();
552 for shape in shape_order {
553 if let Some(pads) = by_shape.remove(shape) {
554 result.push((shape.to_string(), pads));
555 }
556 }
557 for (shape, pads) in by_shape {
559 result.push((shape, pads));
560 }
561 Some(result)
562 } else {
563 None
564 };
565
566 Ok(PcbLibPadList {
567 path: path.display().to_string(),
568 total_pads: all_pads.len(),
569 pads: all_pads,
570 pads_by_shape,
571 })
572}
573
574pub fn cmd_primitives(
576 path: &Path,
577 name: &str,
578) -> Result<PcbLibPrimitiveList, Box<dyn std::error::Error>> {
579 let lib = open_pcblib(path)?;
580
581 let name_lower = name.to_lowercase();
582 let comp = lib
583 .iter()
584 .find(|c| c.pattern.to_lowercase() == name_lower)
585 .ok_or_else(|| format!("Footprint '{}' not found", name))?;
586
587 let primitives = comp
588 .primitives
589 .iter()
590 .map(|prim| match prim {
591 PcbRecord::Pad(p) => {
592 let size = p.size_top();
593 let hole = if p.has_hole() {
594 Some(fmt_coord_val(&p.hole_size))
595 } else {
596 None
597 };
598 PrimitiveDetail::Pad {
599 designator: p.designator.clone(),
600 shape: pad_shape_name(p.shape_top()).to_string(),
601 size: format!("{}x{}", fmt_coord_val(&size.x), fmt_coord_val(&size.y)),
602 hole,
603 }
604 }
605 PcbRecord::Track(t) => PrimitiveDetail::Track {
606 start_x: fmt_coord_val(&t.start.x),
607 start_y: fmt_coord_val(&t.start.y),
608 end_x: fmt_coord_val(&t.end.x),
609 end_y: fmt_coord_val(&t.end.y),
610 width: fmt_coord_val(&t.width),
611 },
612 PcbRecord::Arc(a) => PrimitiveDetail::Arc {
613 center_x: fmt_coord_val(&a.location.x),
614 center_y: fmt_coord_val(&a.location.y),
615 radius: fmt_coord_val(&a.radius),
616 start_angle: a.start_angle,
617 end_angle: a.end_angle,
618 },
619 PcbRecord::Text(t) => PrimitiveDetail::Text {
620 text: t.text.clone(),
621 x: fmt_coord_val(&t.base.corner1.x),
622 y: fmt_coord_val(&t.base.corner1.y),
623 },
624 PcbRecord::Fill(f) => PrimitiveDetail::Fill {
625 x1: fmt_coord_val(&f.base.corner1.x),
626 y1: fmt_coord_val(&f.base.corner1.y),
627 x2: fmt_coord_val(&f.base.corner2.x),
628 y2: fmt_coord_val(&f.base.corner2.y),
629 },
630 PcbRecord::Region(r) => PrimitiveDetail::Region {
631 vertex_count: r.outline.len(),
632 layer: layer_name(&r.common.layer),
633 },
634 PcbRecord::ComponentBody(b) => PrimitiveDetail::ComponentBody {
635 vertex_count: b.outline.len(),
636 height: fmt_coord_val(&b.overall_height),
637 },
638 _ => PrimitiveDetail::Other {
639 primitive_type: record_type_name(prim).to_string(),
640 },
641 })
642 .collect();
643
644 Ok(PcbLibPrimitiveList {
645 footprint_name: comp.pattern.clone(),
646 total_primitives: comp.primitive_count(),
647 primitives,
648 })
649}
650
651pub fn cmd_holes(path: &Path) -> Result<PcbLibHoleAnalysis, Box<dyn std::error::Error>> {
653 let lib = open_pcblib(path)?;
654
655 let mut hole_sizes: HashMap<String, Vec<String>> = HashMap::new();
656
657 for comp in lib.iter() {
658 for pad in comp.pads() {
659 if pad.has_hole() && pad.hole_size.to_raw() > 0 {
660 let size_str = fmt_coord_val(&pad.hole_size);
661 hole_sizes
662 .entry(size_str)
663 .or_default()
664 .push(comp.pattern.clone());
665 }
666 }
667 }
668
669 let mut hole_size_infos: Vec<_> = hole_sizes
670 .into_iter()
671 .map(|(size, footprints)| {
672 let unique_footprints: std::collections::HashSet<_> = footprints.into_iter().collect();
674 let count = unique_footprints.len();
675 let example_footprints: Vec<_> = unique_footprints.into_iter().take(3).collect();
676
677 HoleSizeInfo {
678 size,
679 count,
680 example_footprints,
681 }
682 })
683 .collect();
684
685 hole_size_infos.sort_by(|a, b| b.count.cmp(&a.count));
687
688 Ok(PcbLibHoleAnalysis {
689 path: path.display().to_string(),
690 hole_sizes: hole_size_infos,
691 })
692}
693
694pub fn cmd_json(path: &Path, full: bool) -> Result<PcbLibJson, Box<dyn std::error::Error>> {
696 let lib = open_pcblib(path)?;
697
698 let footprints: Vec<FootprintJsonData> = lib
699 .iter()
700 .map(|comp| {
701 let pads = if full {
702 Some(
703 comp.pads()
704 .map(|pad| {
705 let size = pad.size_top();
706 PadJsonData {
707 designator: pad.designator.clone(),
708 shape: pad_shape_name(pad.shape_top()).to_string(),
709 size_x: fmt_coord_val(&size.x),
710 size_y: fmt_coord_val(&size.y),
711 hole_size: if pad.has_hole() {
712 Some(fmt_coord_val(&pad.hole_size))
713 } else {
714 None
715 },
716 layer: layer_name(&pad.common.layer),
717 }
718 })
719 .collect(),
720 )
721 } else {
722 None
723 };
724
725 FootprintJsonData {
726 name: comp.pattern.clone(),
727 description: comp.description.clone(),
728 pad_count: comp.pad_count(),
729 primitive_count: comp.primitive_count(),
730 pads,
731 }
732 })
733 .collect();
734
735 Ok(PcbLibJson {
736 file: path.display().to_string(),
737 footprint_count: lib.components.len(),
738 unique_id: lib.unique_id.clone(),
739 footprints,
740 })
741}
742
743use crate::footprint::{
748 Measurement, analyze_pitch, generate_report, measure_dimensions, measure_pad,
749 measure_pad_distance, minimum_pad_clearance, pad_to_silkscreen_clearance,
750};
751
752pub fn cmd_measure(
754 path: &Path,
755 name: &str,
756 measure_type: &str,
757 pad1: Option<String>,
758 pad2: Option<String>,
759 pad: Option<String>,
760 output_json: bool,
761) -> Result<(), Box<dyn std::error::Error>> {
762 let lib = open_pcblib(path)?;
763
764 let name_lower = name.to_lowercase();
765 let component = lib
766 .iter()
767 .find(|c| c.pattern.to_lowercase() == name_lower || matches_pattern(&c.pattern, name))
768 .ok_or_else(|| format!("Footprint '{}' not found", name))?;
769
770 match measure_type.to_lowercase().as_str() {
771 "all" | "report" => {
772 let report = generate_report(component);
773
774 if output_json {
775 print_measurement_report_json(&report)?;
776 } else {
777 print_measurement_report(&report);
778 }
779 }
780 "distance" | "dist" => {
781 let p1 = pad1.ok_or("--pad1 required for distance measurement")?;
782 let p2 = pad2.ok_or("--pad2 required for distance measurement")?;
783
784 let dist = measure_pad_distance(component, &p1, &p2).ok_or_else(|| {
785 format!("Could not measure distance between pads {} and {}", p1, p2)
786 })?;
787
788 if output_json {
789 print_distance_json(&dist)?;
790 } else {
791 println!("Distance: {} to {}", dist.pad1, dist.pad2);
792 println!(" Center-to-center: {}", dist.center_to_center.display());
793 println!(" Edge-to-edge: {}", dist.edge_to_edge.display());
794 }
795 }
796 "pitch" => {
797 let pitches = analyze_pitch(component);
798
799 if output_json {
800 print_pitch_json(&pitches)?;
801 } else if pitches.is_empty() {
802 println!("No regular pitch detected (footprint may have irregular pad spacing)");
803 } else {
804 println!("Pitch Analysis for: {}", component.pattern);
805 println!("═══════════════════════════════════════════════════════════════");
806 for pitch_info in &pitches {
807 println!(
808 "\n{} pitch: {}",
809 pitch_info.direction,
810 pitch_info.pitch.display()
811 );
812 println!(" {} pad pairs with this spacing", pitch_info.count);
813 for (p1, p2, dist) in pitch_info.pad_pairs.iter().take(5) {
814 println!(" {} ↔ {}: {}", p1, p2, dist.display());
815 }
816 if pitch_info.pad_pairs.len() > 5 {
817 println!(" ... and {} more pairs", pitch_info.pad_pairs.len() - 5);
818 }
819 }
820 }
821 }
822 "dimensions" | "dims" | "bounds" => {
823 let dims = measure_dimensions(component);
824
825 if output_json {
826 print_dimensions_json(&dims)?;
827 } else {
828 println!("Dimensions for: {}", component.pattern);
829 println!("═══════════════════════════════════════════════════════════════");
830 println!(" Width: {}", dims.width.display());
831 println!(" Height: {}", dims.height.display());
832 println!(
833 " X range: {} to {}",
834 dims.min_x.display(),
835 dims.max_x.display()
836 );
837 println!(
838 " Y range: {} to {}",
839 dims.min_y.display(),
840 dims.max_y.display()
841 );
842 }
843 }
844 "clearance" | "clear" => {
845 let pad_clear = minimum_pad_clearance(component);
846 let silk_clear = pad_to_silkscreen_clearance(component);
847
848 if output_json {
849 print_clearance_json(pad_clear.as_ref(), silk_clear.as_ref())?;
850 } else {
851 println!("Clearance Analysis for: {}", component.pattern);
852 println!("═══════════════════════════════════════════════════════════════");
853
854 if let Some(pc) = pad_clear {
855 println!("\nMinimum pad-to-pad clearance: {}", pc.clearance.display());
856 println!(" Location: {}", pc.location);
857 } else {
858 println!("\nNo pad-to-pad clearance (single pad or overlapping pads)");
859 }
860
861 if let Some(sc) = silk_clear {
862 println!("\nPad-to-silkscreen clearance: {}", sc.clearance.display());
863 println!(" Location: {}", sc.location);
864 } else {
865 println!("\nNo silkscreen elements found");
866 }
867 }
868 }
869 "pad" => {
870 let des = pad.ok_or("--pad required for pad measurement")?;
871 let info =
872 measure_pad(component, &des).ok_or_else(|| format!("Pad '{}' not found", des))?;
873
874 if output_json {
875 print_pad_json(&info)?;
876 } else {
877 println!("Pad {} info:", info.designator);
878 println!("═══════════════════════════════════════════════════════════════");
879 println!(" Position: ({}, {})", info.x.display(), info.y.display());
880 println!(
881 " Size: {} x {}",
882 info.width.display(),
883 info.height.display()
884 );
885 println!(" Shape: {}", info.shape);
886 if let Some(hole) = &info.hole {
887 println!(" Hole: {}", hole.display());
888 } else {
889 println!(" Type: SMD");
890 }
891 }
892 }
893 "pads" => {
894 let report = generate_report(component);
895
896 if output_json {
897 print_all_pads_json(&report.pads)?;
898 } else {
899 println!("All Pads for: {}", component.pattern);
900 println!("═══════════════════════════════════════════════════════════════");
901 println!(
902 "\n{:<6} {:>10} {:>10} {:>10} {:>10} {:>10} Shape",
903 "Pad", "X (mm)", "Y (mm)", "W (mm)", "H (mm)", "Hole"
904 );
905 println!(
906 "{:-<6} {:->10} {:->10} {:->10} {:->10} {:->10} {:-<12}",
907 "", "", "", "", "", "", ""
908 );
909
910 for pad_info in &report.pads {
911 let hole_str = pad_info
912 .hole
913 .as_ref()
914 .map(|h| format!("{:.3}", h.mm))
915 .unwrap_or_else(|| "-".to_string());
916
917 println!(
918 "{:<6} {:>10.3} {:>10.3} {:>10.3} {:>10.3} {:>10} {}",
919 pad_info.designator,
920 pad_info.x.mm,
921 pad_info.y.mm,
922 pad_info.width.mm,
923 pad_info.height.mm,
924 hole_str,
925 pad_info.shape
926 );
927 }
928 }
929 }
930 _ => {
931 return Err(format!(
932 "Unknown measurement type: '{}'. Use: all, distance, pitch, dimensions, clearance, pad, pads",
933 measure_type
934 ).into());
935 }
936 }
937
938 Ok(())
939}
940
941fn print_measurement_report(report: &crate::footprint::MeasurementReport) {
943 println!("╔═══════════════════════════════════════════════════════════════╗");
944 println!("║ FOOTPRINT MEASUREMENT REPORT ║");
945 println!("╚═══════════════════════════════════════════════════════════════╝");
946 println!("\nFootprint: {}", report.name);
947
948 println!("\n┌─────────────────────────────────────────────────────────────────┐");
950 println!("│ DIMENSIONS │");
951 println!("└─────────────────────────────────────────────────────────────────┘");
952 println!(" Width: {}", report.dimensions.width.display());
953 println!(" Height: {}", report.dimensions.height.display());
954
955 if let Some(span) = &report.row_span {
956 println!(" Row span: {}", span.display());
957 }
958
959 println!("\n┌─────────────────────────────────────────────────────────────────┐");
961 println!(
962 "│ PADS ({} total) │",
963 report.pads.len()
964 );
965 println!("└─────────────────────────────────────────────────────────────────┘");
966
967 println!(
968 "\n{:<6} {:>10} {:>10} {:>10} {:>10} Shape",
969 "Pad", "X (mm)", "Y (mm)", "W (mm)", "H (mm)"
970 );
971 println!(
972 "{:-<6} {:->10} {:->10} {:->10} {:->10} {:-<12}",
973 "", "", "", "", "", ""
974 );
975
976 for pad in &report.pads {
977 println!(
978 "{:<6} {:>10.3} {:>10.3} {:>10.3} {:>10.3} {}",
979 pad.designator, pad.x.mm, pad.y.mm, pad.width.mm, pad.height.mm, pad.shape
980 );
981 }
982
983 if !report.pitch.is_empty() {
985 println!("\n┌─────────────────────────────────────────────────────────────────┐");
986 println!("│ PITCH ANALYSIS │");
987 println!("└─────────────────────────────────────────────────────────────────┘");
988
989 for pitch_info in &report.pitch {
990 println!(
991 "\n {} pitch: {}",
992 pitch_info.direction,
993 pitch_info.pitch.display()
994 );
995 println!(" {} adjacent pad pairs", pitch_info.count);
996 }
997 }
998
999 println!("\n┌─────────────────────────────────────────────────────────────────┐");
1001 println!("│ CLEARANCES │");
1002 println!("└─────────────────────────────────────────────────────────────────┘");
1003
1004 if let Some(pc) = &report.min_pad_clearance {
1005 println!("\n Minimum pad-to-pad gap: {}", pc.clearance.display());
1006 println!(" {}", pc.location);
1007 }
1008
1009 if let Some(sc) = &report.silkscreen_clearance {
1010 println!("\n Pad-to-silkscreen: {}", sc.clearance.display());
1011 println!(" {}", sc.location);
1012 }
1013}
1014
1015fn print_measurement_report_json(
1018 report: &crate::footprint::MeasurementReport,
1019) -> Result<(), Box<dyn std::error::Error>> {
1020 #[derive(Serialize)]
1021 struct MeasurementJson {
1022 mm: f64,
1023 mils: f64,
1024 }
1025
1026 impl From<&Measurement> for MeasurementJson {
1027 fn from(m: &Measurement) -> Self {
1028 MeasurementJson {
1029 mm: m.mm,
1030 mils: m.mils,
1031 }
1032 }
1033 }
1034
1035 #[derive(Serialize)]
1036 struct PadInfoJson {
1037 designator: String,
1038 x_mm: f64,
1039 y_mm: f64,
1040 width_mm: f64,
1041 height_mm: f64,
1042 hole_mm: Option<f64>,
1043 shape: String,
1044 }
1045
1046 #[derive(Serialize)]
1047 struct PitchJson {
1048 pitch: MeasurementJson,
1049 direction: String,
1050 count: usize,
1051 }
1052
1053 #[derive(Serialize)]
1054 struct ClearanceJson {
1055 feature1: String,
1056 feature2: String,
1057 clearance: MeasurementJson,
1058 location: String,
1059 }
1060
1061 #[derive(Serialize)]
1062 struct ReportJson {
1063 name: String,
1064 dimensions: DimensionsJson,
1065 pads: Vec<PadInfoJson>,
1066 pitch: Vec<PitchJson>,
1067 min_pad_clearance: Option<ClearanceJson>,
1068 silkscreen_clearance: Option<ClearanceJson>,
1069 row_span: Option<MeasurementJson>,
1070 }
1071
1072 #[derive(Serialize)]
1073 struct DimensionsJson {
1074 width: MeasurementJson,
1075 height: MeasurementJson,
1076 min_x: MeasurementJson,
1077 max_x: MeasurementJson,
1078 min_y: MeasurementJson,
1079 max_y: MeasurementJson,
1080 }
1081
1082 let output = ReportJson {
1083 name: report.name.clone(),
1084 dimensions: DimensionsJson {
1085 width: (&report.dimensions.width).into(),
1086 height: (&report.dimensions.height).into(),
1087 min_x: (&report.dimensions.min_x).into(),
1088 max_x: (&report.dimensions.max_x).into(),
1089 min_y: (&report.dimensions.min_y).into(),
1090 max_y: (&report.dimensions.max_y).into(),
1091 },
1092 pads: report
1093 .pads
1094 .iter()
1095 .map(|p| PadInfoJson {
1096 designator: p.designator.clone(),
1097 x_mm: p.x.mm,
1098 y_mm: p.y.mm,
1099 width_mm: p.width.mm,
1100 height_mm: p.height.mm,
1101 hole_mm: p.hole.as_ref().map(|h| h.mm),
1102 shape: p.shape.clone(),
1103 })
1104 .collect(),
1105 pitch: report
1106 .pitch
1107 .iter()
1108 .map(|p| PitchJson {
1109 pitch: (&p.pitch).into(),
1110 direction: p.direction.clone(),
1111 count: p.count,
1112 })
1113 .collect(),
1114 min_pad_clearance: report.min_pad_clearance.as_ref().map(|c| ClearanceJson {
1115 feature1: c.feature1.clone(),
1116 feature2: c.feature2.clone(),
1117 clearance: (&c.clearance).into(),
1118 location: c.location.clone(),
1119 }),
1120 silkscreen_clearance: report.silkscreen_clearance.as_ref().map(|c| ClearanceJson {
1121 feature1: c.feature1.clone(),
1122 feature2: c.feature2.clone(),
1123 clearance: (&c.clearance).into(),
1124 location: c.location.clone(),
1125 }),
1126 row_span: report.row_span.as_ref().map(|s| s.into()),
1127 };
1128
1129 let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
1130 println!("{}", json);
1131 Ok(())
1132}
1133
1134fn print_distance_json(
1135 dist: &crate::footprint::PadDistance,
1136) -> Result<(), Box<dyn std::error::Error>> {
1137 #[derive(Serialize)]
1138 struct DistanceJson {
1139 pad1: String,
1140 pad2: String,
1141 center_to_center_mm: f64,
1142 center_to_center_mils: f64,
1143 edge_to_edge_mm: f64,
1144 edge_to_edge_mils: f64,
1145 }
1146
1147 let output = DistanceJson {
1148 pad1: dist.pad1.clone(),
1149 pad2: dist.pad2.clone(),
1150 center_to_center_mm: dist.center_to_center.mm,
1151 center_to_center_mils: dist.center_to_center.mils,
1152 edge_to_edge_mm: dist.edge_to_edge.mm,
1153 edge_to_edge_mils: dist.edge_to_edge.mils,
1154 };
1155
1156 let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
1157 println!("{}", json);
1158 Ok(())
1159}
1160
1161fn print_pitch_json(
1162 pitches: &[crate::footprint::PitchAnalysis],
1163) -> Result<(), Box<dyn std::error::Error>> {
1164 #[derive(Serialize)]
1165 struct PitchJson {
1166 direction: String,
1167 pitch_mm: f64,
1168 pitch_mils: f64,
1169 pad_pair_count: usize,
1170 }
1171
1172 let output: Vec<PitchJson> = pitches
1173 .iter()
1174 .map(|p| PitchJson {
1175 direction: p.direction.clone(),
1176 pitch_mm: p.pitch.mm,
1177 pitch_mils: p.pitch.mils,
1178 pad_pair_count: p.count,
1179 })
1180 .collect();
1181
1182 let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
1183 println!("{}", json);
1184 Ok(())
1185}
1186
1187fn print_dimensions_json(
1188 dims: &crate::footprint::FootprintDimensions,
1189) -> Result<(), Box<dyn std::error::Error>> {
1190 #[derive(Serialize)]
1191 struct DimsJson {
1192 width_mm: f64,
1193 width_mils: f64,
1194 height_mm: f64,
1195 height_mils: f64,
1196 min_x_mm: f64,
1197 max_x_mm: f64,
1198 min_y_mm: f64,
1199 max_y_mm: f64,
1200 }
1201
1202 let output = DimsJson {
1203 width_mm: dims.width.mm,
1204 width_mils: dims.width.mils,
1205 height_mm: dims.height.mm,
1206 height_mils: dims.height.mils,
1207 min_x_mm: dims.min_x.mm,
1208 max_x_mm: dims.max_x.mm,
1209 min_y_mm: dims.min_y.mm,
1210 max_y_mm: dims.max_y.mm,
1211 };
1212
1213 let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
1214 println!("{}", json);
1215 Ok(())
1216}
1217
1218fn print_clearance_json(
1219 pad_clear: Option<&crate::footprint::ClearanceResult>,
1220 silk_clear: Option<&crate::footprint::ClearanceResult>,
1221) -> Result<(), Box<dyn std::error::Error>> {
1222 #[derive(Serialize)]
1223 struct ClearanceJson {
1224 feature1: String,
1225 feature2: String,
1226 clearance_mm: f64,
1227 clearance_mils: f64,
1228 location: String,
1229 }
1230
1231 #[derive(Serialize)]
1232 struct Output {
1233 pad_to_pad: Option<ClearanceJson>,
1234 pad_to_silkscreen: Option<ClearanceJson>,
1235 }
1236
1237 let output = Output {
1238 pad_to_pad: pad_clear.map(|c| ClearanceJson {
1239 feature1: c.feature1.clone(),
1240 feature2: c.feature2.clone(),
1241 clearance_mm: c.clearance.mm,
1242 clearance_mils: c.clearance.mils,
1243 location: c.location.clone(),
1244 }),
1245 pad_to_silkscreen: silk_clear.map(|c| ClearanceJson {
1246 feature1: c.feature1.clone(),
1247 feature2: c.feature2.clone(),
1248 clearance_mm: c.clearance.mm,
1249 clearance_mils: c.clearance.mils,
1250 location: c.location.clone(),
1251 }),
1252 };
1253
1254 let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
1255 println!("{}", json);
1256 Ok(())
1257}
1258
1259fn print_pad_json(info: &crate::footprint::PadInfo) -> Result<(), Box<dyn std::error::Error>> {
1260 #[derive(Serialize)]
1261 struct PadJson {
1262 designator: String,
1263 x_mm: f64,
1264 y_mm: f64,
1265 width_mm: f64,
1266 height_mm: f64,
1267 hole_mm: Option<f64>,
1268 shape: String,
1269 is_smd: bool,
1270 }
1271
1272 let output = PadJson {
1273 designator: info.designator.clone(),
1274 x_mm: info.x.mm,
1275 y_mm: info.y.mm,
1276 width_mm: info.width.mm,
1277 height_mm: info.height.mm,
1278 hole_mm: info.hole.as_ref().map(|h| h.mm),
1279 shape: info.shape.clone(),
1280 is_smd: info.hole.is_none(),
1281 };
1282
1283 let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
1284 println!("{}", json);
1285 Ok(())
1286}
1287
1288fn print_all_pads_json(
1289 pads: &[crate::footprint::PadInfo],
1290) -> Result<(), Box<dyn std::error::Error>> {
1291 #[derive(Serialize)]
1292 struct PadJson {
1293 designator: String,
1294 x_mm: f64,
1295 y_mm: f64,
1296 width_mm: f64,
1297 height_mm: f64,
1298 hole_mm: Option<f64>,
1299 shape: String,
1300 }
1301
1302 let output: Vec<PadJson> = pads
1303 .iter()
1304 .map(|p| PadJson {
1305 designator: p.designator.clone(),
1306 x_mm: p.x.mm,
1307 y_mm: p.y.mm,
1308 width_mm: p.width.mm,
1309 height_mm: p.height.mm,
1310 hole_mm: p.hole.as_ref().map(|h| h.mm),
1311 shape: p.shape.clone(),
1312 })
1313 .collect();
1314
1315 let json = serde_json::to_string_pretty(&output).map_err(|e| e.to_string())?;
1316 println!("{}", json);
1317 Ok(())
1318}
1319
1320const BLANK_PCBLIB_TEMPLATE: &[u8] = include_bytes!("../../data/blank/PcbLib1.PcbLib");
1326
1327use crate::footprint::{ChipSpec, IpcDensity};
1328use crate::records::pcb::{PcbArc, PcbComponent, PcbFlags, PcbPrimitiveCommon, PcbTrack};
1329use crate::types::{Coord, CoordPoint};
1330
1331pub fn cmd_create(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
1333 if path.exists() {
1334 return Err(format!("File already exists: {}", path.display()).into());
1335 }
1336
1337 std::fs::write(path, BLANK_PCBLIB_TEMPLATE)
1338 .map_err(|e| format!("Error creating file: {}", e))?;
1339
1340 println!("Created empty PcbLib: {}", path.display());
1341 Ok(())
1342}
1343
1344fn load_blank_pcblib() -> Result<PcbLib, Box<dyn std::error::Error>> {
1345 Ok(PcbLib::open(Cursor::new(BLANK_PCBLIB_TEMPLATE))?)
1346}
1347
1348pub fn cmd_add_footprint(
1350 path: &Path,
1351 name: &str,
1352 description: Option<String>,
1353) -> Result<(), Box<dyn std::error::Error>> {
1354 let mut lib = open_or_create_pcblib(path)?;
1355
1356 if lib.components.iter().any(|c| c.pattern == name) {
1358 return Err(format!("Footprint '{}' already exists", name).into());
1359 }
1360
1361 let mut det = ();
1362 let mut component = PcbComponent::new_deterministic(name, &mut det);
1363 if let Some(desc) = description {
1364 component.set_description(desc);
1365 }
1366
1367 lib.components.push(component);
1368 save_pcblib(path, &lib)?;
1369
1370 println!("Added footprint '{}' to {}", name, path.display());
1371 Ok(())
1372}
1373
1374#[allow(clippy::too_many_arguments)]
1376pub fn cmd_add_pad(
1377 path: &Path,
1378 footprint: &str,
1379 designator: &str,
1380 x: f64,
1381 y: f64,
1382 width: f64,
1383 height: f64,
1384 shape_str: &str,
1385 hole: f64,
1386) -> Result<(), Box<dyn std::error::Error>> {
1387 let mut lib = open_pcblib(path)?;
1388
1389 let component = lib
1390 .components
1391 .iter_mut()
1392 .find(|c| c.pattern == footprint)
1393 .ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
1394
1395 let shape = match shape_str.to_lowercase().as_str() {
1397 "round" => PcbPadShape::Round,
1398 "rectangular" | "rect" => PcbPadShape::Rectangular,
1399 "rounded_rect" | "roundedrect" => PcbPadShape::RoundedRectangle,
1400 "octagonal" | "oct" => PcbPadShape::Octagonal,
1401 _ => return Err(format!("Unknown pad shape: {}", shape_str).into()),
1402 };
1403
1404 let mut builder = FootprintBuilder::new(footprint);
1406 if hole > 0.0 {
1407 builder.add_th_pad(designator, x, y, width.max(height), hole, shape);
1408 } else {
1409 builder.add_smd_pad(designator, x, y, width, height, shape);
1410 }
1411
1412 let mut det = ();
1414 let temp = builder.build_deterministic(&mut det);
1415 if let Some(PcbRecord::Pad(pad)) = temp.primitives.into_iter().next() {
1416 component.add_primitive(PcbRecord::Pad(pad));
1417 }
1418
1419 save_pcblib(path, &lib)?;
1420 println!(
1421 "Added pad '{}' to footprint '{}' at ({}, {}) mm",
1422 designator, footprint, x, y
1423 );
1424 Ok(())
1425}
1426
1427pub fn cmd_add_silkscreen(
1429 path: &Path,
1430 footprint: &str,
1431 x1: f64,
1432 y1: f64,
1433 x2: f64,
1434 y2: f64,
1435 width: f64,
1436) -> Result<(), Box<dyn std::error::Error>> {
1437 let mut lib = open_pcblib(path)?;
1438
1439 let component = lib
1440 .components
1441 .iter_mut()
1442 .find(|c| c.pattern == footprint)
1443 .ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
1444
1445 let track = PcbTrack {
1446 common: PcbPrimitiveCommon {
1447 layer: Layer::TOP_OVERLAY,
1448 flags: PcbFlags::UNLOCKED | PcbFlags::UNKNOWN8,
1449 unique_id: None,
1450 },
1451 start: CoordPoint::from_mms(x1, y1),
1452 end: CoordPoint::from_mms(x2, y2),
1453 width: Coord::from_mms(width),
1454 unknown: vec![0u8; 16],
1455 };
1456
1457 component.add_primitive(PcbRecord::Track(track));
1458 save_pcblib(path, &lib)?;
1459
1460 println!("Added silkscreen line to footprint '{}'", footprint);
1461 Ok(())
1462}
1463
1464#[allow(clippy::too_many_arguments)]
1466pub fn cmd_add_arc(
1467 path: &Path,
1468 footprint: &str,
1469 x: f64,
1470 y: f64,
1471 radius: f64,
1472 start_angle: f64,
1473 end_angle: f64,
1474 width: f64,
1475) -> Result<(), Box<dyn std::error::Error>> {
1476 let mut lib = open_pcblib(path)?;
1477
1478 let component = lib
1479 .components
1480 .iter_mut()
1481 .find(|c| c.pattern == footprint)
1482 .ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
1483
1484 let arc = PcbArc {
1485 common: PcbPrimitiveCommon {
1486 layer: Layer::TOP_OVERLAY,
1487 flags: PcbFlags::UNLOCKED | PcbFlags::UNKNOWN8,
1488 unique_id: None,
1489 },
1490 location: CoordPoint::from_mms(x, y),
1491 radius: Coord::from_mms(radius),
1492 start_angle,
1493 end_angle,
1494 width: Coord::from_mms(width),
1495 };
1496
1497 component.add_primitive(PcbRecord::Arc(arc));
1498 save_pcblib(path, &lib)?;
1499
1500 println!(
1501 "Added silkscreen arc to footprint '{}' (center: ({}, {}) mm, radius: {} mm, {:.0}° to {:.0}°)",
1502 footprint, x, y, radius, start_angle, end_angle
1503 );
1504 Ok(())
1505}
1506
1507pub fn cmd_gen_chip(
1509 path: &Path,
1510 size: &str,
1511 density_str: &str,
1512) -> Result<(), Box<dyn std::error::Error>> {
1513 let mut lib = open_or_create_pcblib(path)?;
1514
1515 let spec = match size.to_uppercase().as_str() {
1516 "0201" => ChipSpec::chip_0201(),
1517 "0402" => ChipSpec::chip_0402(),
1518 "0603" => ChipSpec::chip_0603(),
1519 "0805" => ChipSpec::chip_0805(),
1520 "1206" => ChipSpec::chip_1206(),
1521 _ => {
1522 return Err(format!(
1523 "Unknown chip size: {}. Supported: 0201, 0402, 0603, 0805, 1206",
1524 size
1525 )
1526 .into());
1527 }
1528 };
1529
1530 let density = parse_density(density_str)?;
1531 let mut det = ();
1532 let component = spec.to_footprint(density).build_deterministic(&mut det);
1533 let name = component.pattern.clone();
1534
1535 if lib.components.iter().any(|c| c.pattern == name) {
1537 return Err(format!("Footprint '{}' already exists", name).into());
1538 }
1539
1540 lib.components.push(component);
1541 save_pcblib(path, &lib)?;
1542
1543 println!(
1544 "Generated chip footprint '{}' with {} density",
1545 name, density_str
1546 );
1547 Ok(())
1548}
1549
1550fn parse_density(s: &str) -> Result<IpcDensity, Box<dyn std::error::Error>> {
1551 match s.to_lowercase().as_str() {
1552 "most" | "a" | "dense" => Ok(IpcDensity::MostDense),
1553 "nominal" | "b" | "normal" => Ok(IpcDensity::Nominal),
1554 "least" | "c" | "loose" => Ok(IpcDensity::LeastDense),
1555 _ => Err(format!("Unknown density: {}. Use: most, nominal, least", s).into()),
1556 }
1557}
1558
1559pub fn cmd_render_svg(
1560 path: &Path,
1561 name: &str,
1562 output: Option<PathBuf>,
1563 scale: f64,
1564 light: bool,
1565 no_grid: bool,
1566 no_designators: bool,
1567) -> Result<(), Box<dyn std::error::Error>> {
1568 use std::fs;
1569
1570 let lib = open_pcblib(path)?;
1571
1572 let name_lower = name.to_lowercase();
1574 let component = lib
1575 .iter()
1576 .find(|c| c.pattern.to_lowercase() == name_lower || matches_pattern(&c.pattern, name))
1577 .ok_or_else(|| format!("Footprint '{}' not found", name))?;
1578
1579 let mut options = if light {
1581 SvgOptions::light()
1582 } else {
1583 SvgOptions::default()
1584 };
1585 options.scale = scale;
1586 options.show_grid = !no_grid;
1587 options.show_designators = !no_designators;
1588
1589 let svg = render_svg(component, &options);
1591
1592 let output_path = output.unwrap_or_else(|| {
1594 PathBuf::from(format!(
1595 "{}.svg",
1596 component.pattern.replace(['/', '\\', ' '], "_")
1597 ))
1598 });
1599
1600 fs::write(&output_path, &svg).map_err(|e| format!("Error writing SVG: {}", e))?;
1602
1603 println!(
1604 "Rendered footprint '{}' to {}",
1605 component.pattern,
1606 output_path.display()
1607 );
1608 println!(" Size: {} bytes", svg.len());
1609 println!(" Theme: {}", if light { "light" } else { "dark" });
1610 println!(" Scale: {} px/mil", scale);
1611
1612 Ok(())
1613}
1614
1615pub fn cmd_render_png(
1616 path: &Path,
1617 name: &str,
1618 output: Option<PathBuf>,
1619 scale: f64,
1620 target_width: Option<u32>,
1621) -> Result<(), Box<dyn std::error::Error>> {
1622 use std::fs::File;
1623 use std::io::BufWriter;
1624
1625 let lib = open_pcblib(path)?;
1626
1627 let name_lower = name.to_lowercase();
1629 let component = lib
1630 .iter()
1631 .find(|c| c.pattern.to_lowercase() == name_lower || matches_pattern(&c.pattern, name))
1632 .ok_or_else(|| format!("Footprint '{}' not found", name))?;
1633
1634 let options = SvgOptions {
1636 scale,
1637 show_grid: false, show_designators: true,
1639 ..Default::default()
1640 };
1641
1642 let svg_data = render_svg(component, &options);
1644
1645 let tree = resvg::usvg::Tree::from_str(&svg_data, &resvg::usvg::Options::default())
1647 .map_err(|e| format!("Error parsing SVG: {}", e))?;
1648
1649 let svg_size = tree.size();
1651 let (width, height) = if let Some(w) = target_width {
1652 let h = (w as f32 * svg_size.height() / svg_size.width()) as u32;
1653 (w, h)
1654 } else {
1655 (svg_size.width() as u32, svg_size.height() as u32)
1656 };
1657
1658 let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height)
1660 .ok_or_else(|| "Failed to create pixmap".to_string())?;
1661
1662 pixmap.fill(resvg::tiny_skia::Color::from_rgba8(30, 30, 30, 255));
1664
1665 let scale_x = width as f32 / svg_size.width();
1667 let scale_y = height as f32 / svg_size.height();
1668 let transform = resvg::tiny_skia::Transform::from_scale(scale_x, scale_y);
1669
1670 resvg::render(&tree, transform, &mut pixmap.as_mut());
1671
1672 let output_path = output.unwrap_or_else(|| {
1674 PathBuf::from(format!(
1675 "{}.png",
1676 component.pattern.replace(['/', '\\', ' '], "_")
1677 ))
1678 });
1679
1680 let file = File::create(&output_path).map_err(|e| format!("Error creating file: {}", e))?;
1682 let writer = BufWriter::new(file);
1683 let mut encoder = png::Encoder::new(writer, width, height);
1684 encoder.set_color(png::ColorType::Rgba);
1685 encoder.set_depth(png::BitDepth::Eight);
1686
1687 let mut png_writer = encoder
1688 .write_header()
1689 .map_err(|e| format!("Error writing PNG header: {}", e))?;
1690 png_writer
1691 .write_image_data(pixmap.data())
1692 .map_err(|e| format!("Error writing PNG data: {}", e))?;
1693
1694 println!(
1695 "Rendered footprint '{}' to {}",
1696 component.pattern,
1697 output_path.display()
1698 );
1699 println!(" Size: {}x{} pixels", width, height);
1700
1701 Ok(())
1702}
1703
1704pub fn cmd_render_ascii(
1705 path: &Path,
1706 name: &str,
1707 max_width: usize,
1708 max_height: usize,
1709) -> Result<(), Box<dyn std::error::Error>> {
1710 let lib = open_pcblib(path)?;
1711
1712 let name_lower = name.to_lowercase();
1714 let component = lib
1715 .iter()
1716 .find(|c| c.pattern.to_lowercase() == name_lower || matches_pattern(&c.pattern, name))
1717 .ok_or_else(|| format!("Footprint '{}' not found", name))?;
1718
1719 let options = AsciiOptions {
1721 max_width,
1722 max_height,
1723 ..Default::default()
1724 };
1725
1726 let ascii = render_ascii(component, &options);
1728 println!("{}", ascii);
1729
1730 Ok(())
1731}
1732
1733fn open_or_create_pcblib(path: &Path) -> Result<PcbLib, Box<dyn std::error::Error>> {
1736 if path.exists() {
1737 open_pcblib(path)
1738 } else {
1739 load_blank_pcblib()
1740 }
1741}
1742
1743fn save_pcblib(path: &Path, lib: &PcbLib) -> Result<(), Box<dyn std::error::Error>> {
1744 Ok(lib.save_to_file(path)?)
1745}
1746
1747fn matches_pattern(text: &str, pattern: &str) -> bool {
1749 let text = text.to_lowercase();
1750 let pattern = pattern.to_lowercase();
1751
1752 fn matches(text: &[char], pattern: &[char]) -> bool {
1753 match (text.first(), pattern.first()) {
1754 (None, None) => true,
1755 (None, Some('*')) => matches(text, &pattern[1..]),
1756 (None, Some(_)) => false,
1757 (Some(_), None) => false,
1758 (Some(_), Some('*')) => {
1759 matches(text, &pattern[1..]) || matches(&text[1..], pattern)
1761 }
1762 (Some(_), Some('?')) => {
1763 matches(&text[1..], &pattern[1..])
1765 }
1766 (Some(t), Some(p)) => *t == *p && matches(&text[1..], &pattern[1..]),
1767 }
1768 }
1769
1770 let text_chars: Vec<char> = text.chars().collect();
1771 let pattern_chars: Vec<char> = pattern.chars().collect();
1772 matches(&text_chars, &pattern_chars)
1773}
1774
1775#[derive(Debug, Clone, Deserialize, Serialize)]
1782pub struct PadJson {
1783 pub designator: String,
1785 pub x: f64,
1787 pub y: f64,
1789 pub width: f64,
1791 pub height: f64,
1793 #[serde(default = "default_pad_shape")]
1795 pub shape: String,
1796 #[serde(default)]
1798 pub hole: f64,
1799 #[serde(default)]
1801 pub rotation: f64,
1802}
1803
1804fn default_pad_shape() -> String {
1805 "rectangular".to_string()
1806}
1807
1808#[derive(Debug, Clone, Deserialize, Serialize)]
1811pub struct LineJson {
1812 pub x1: f64,
1814 pub y1: f64,
1816 pub x2: f64,
1818 pub y2: f64,
1820 #[serde(default = "default_line_width")]
1822 pub width: f64,
1823}
1824
1825fn default_line_width() -> f64 {
1826 0.15
1827}
1828
1829#[derive(Debug, Clone, Deserialize, Serialize)]
1832pub struct ArcJson {
1833 pub x: f64,
1835 pub y: f64,
1837 pub radius: f64,
1839 pub start_angle: f64,
1841 pub end_angle: f64,
1843 #[serde(default = "default_line_width")]
1845 pub width: f64,
1846}
1847
1848#[derive(Debug, Clone, Deserialize, Serialize)]
1851pub struct TextJson {
1852 pub x: f64,
1854 pub y: f64,
1856 pub text: String,
1858 #[serde(default = "default_text_height")]
1860 pub height: f64,
1861 #[serde(default)]
1863 pub rotation: f64,
1864 #[serde(default = "default_stroke_width")]
1866 pub stroke_width: f64,
1867 #[serde(default = "default_text_layer")]
1869 pub layer: String,
1870 #[serde(default)]
1872 pub mirrored: bool,
1873}
1874
1875fn default_text_height() -> f64 {
1876 1.0
1877}
1878
1879fn default_stroke_width() -> f64 {
1880 0.15
1881}
1882
1883fn default_text_layer() -> String {
1884 "top_overlay".to_string()
1885}
1886
1887#[derive(Debug, Clone, Deserialize, Serialize)]
1894pub struct PadRowJson {
1895 pub count: usize,
1897 pub pitch: String,
1899 pub pad_width: String,
1901 pub pad_height: String,
1903 #[serde(default = "default_direction")]
1905 pub direction: String,
1906 #[serde(default = "default_start")]
1908 pub start: u32,
1909 #[serde(default)]
1911 pub x: String,
1912 #[serde(default)]
1914 pub y: String,
1915 #[serde(default = "default_pad_shape_str")]
1917 pub shape: String,
1918 #[serde(default)]
1920 pub hole: String,
1921 #[serde(default)]
1923 pub use_spacing: bool,
1924}
1925
1926#[derive(Debug, Clone, Deserialize, Serialize)]
1928pub struct DualRowJson {
1929 pub pads_per_side: usize,
1931 pub pitch: String,
1933 pub row_spacing: String,
1935 #[serde(default)]
1937 pub pad_width: Option<String>,
1938 #[serde(default)]
1940 pub pad_height: Option<String>,
1941 #[serde(default)]
1943 pub pad_diameter: Option<String>,
1944 #[serde(default)]
1946 pub hole: Option<String>,
1947 #[serde(default = "default_pad_shape_str")]
1949 pub shape: String,
1950}
1951
1952#[derive(Debug, Clone, Deserialize, Serialize)]
1954pub struct QuadPadsJson {
1955 pub pads_per_side: usize,
1957 pub pitch: String,
1959 pub span: String,
1961 pub pad_width: String,
1963 pub pad_height: String,
1965 #[serde(default = "default_pad_shape_str")]
1967 pub shape: String,
1968}
1969
1970#[derive(Debug, Clone, Deserialize, Serialize)]
1972pub struct PadGridJson {
1973 pub rows: usize,
1975 pub cols: usize,
1977 pub pitch: String,
1979 pub pad_diameter: String,
1981 #[serde(default = "default_round_shape")]
1983 pub shape: String,
1984 #[serde(default)]
1986 pub skip_center: String,
1987}
1988
1989fn default_direction() -> String {
1990 "horizontal".to_string()
1991}
1992
1993fn default_start() -> u32 {
1994 1
1995}
1996
1997fn default_pad_shape_str() -> String {
1998 "rectangular".to_string()
1999}
2000
2001fn default_round_shape() -> String {
2002 "round".to_string()
2003}
2004
2005#[derive(Debug, Clone, Deserialize, Serialize)]
2011pub struct FootprintJson {
2012 pub name: String,
2014 #[serde(default)]
2016 pub description: String,
2017
2018 #[serde(default)]
2021 pub pads: Vec<PadJson>,
2022 #[serde(default)]
2024 pub lines: Vec<LineJson>,
2025 #[serde(default)]
2027 pub arcs: Vec<ArcJson>,
2028 #[serde(default)]
2030 pub texts: Vec<TextJson>,
2031
2032 #[serde(default)]
2035 pub pad_rows: Vec<PadRowJson>,
2036 #[serde(default)]
2038 pub dual_rows: Vec<DualRowJson>,
2039 #[serde(default)]
2041 pub quad_pads: Vec<QuadPadsJson>,
2042 #[serde(default)]
2044 pub pad_grids: Vec<PadGridJson>,
2045}
2046
2047pub fn cmd_add_json(
2049 path: &Path,
2050 json_file: Option<String>,
2051 json_str: Option<String>,
2052) -> Result<(), Box<dyn std::error::Error>> {
2053 use std::io::{self, Read as IoRead};
2054
2055 let json_content = match (json_file, json_str) {
2057 (_, Some(s)) => s,
2058 (Some(ref path), None) if path == "-" => {
2059 let mut buffer = String::new();
2060 io::stdin()
2061 .read_to_string(&mut buffer)
2062 .map_err(|e| format!("Error reading from stdin: {}", e))?;
2063 buffer
2064 }
2065 (Some(ref file_path), None) => std::fs::read_to_string(file_path)
2066 .map_err(|e| format!("Error reading file '{}': {}", file_path, e))?,
2067 (None, None) => {
2068 return Err("Must provide either --file <path> or --json <string>"
2069 .to_string()
2070 .into());
2071 }
2072 };
2073
2074 let footprint_def: FootprintJson =
2076 serde_json::from_str(&json_content).map_err(|e| format!("Invalid JSON: {}", e))?;
2077
2078 let mut lib = open_or_create_pcblib(path)?;
2080
2081 if lib
2083 .components
2084 .iter()
2085 .any(|c| c.pattern == footprint_def.name)
2086 {
2087 return Err(format!("Footprint '{}' already exists", footprint_def.name).into());
2088 }
2089
2090 let mut builder = FootprintBuilder::new(&footprint_def.name);
2092
2093 if !footprint_def.description.is_empty() {
2094 builder = builder.description(&footprint_def.description);
2095 }
2096
2097 for row in &footprint_def.pad_rows {
2101 let pitch_mm = parse_unit_value_or_mm(&row.pitch)?;
2102 let pad_width_mm = parse_unit_value_or_mm(&row.pad_width)?;
2103 let pad_height_mm = parse_unit_value_or_mm(&row.pad_height)?;
2104 let x_mm = if row.x.is_empty() {
2105 0.0
2106 } else {
2107 parse_unit_value_or_mm(&row.x)?
2108 };
2109 let y_mm = if row.y.is_empty() {
2110 0.0
2111 } else {
2112 parse_unit_value_or_mm(&row.y)?
2113 };
2114 let hole_mm = if row.hole.is_empty() {
2115 0.0
2116 } else {
2117 parse_unit_value_or_mm(&row.hole)?
2118 };
2119 let dir = PadRowDirection::try_parse(&row.direction)
2120 .ok_or_else(|| format!("Invalid direction '{}' in pad_row", row.direction))?;
2121 let shape = parse_pad_shape(&row.shape)?;
2122
2123 if hole_mm > 0.0 {
2124 let pad_diameter = pad_width_mm.max(pad_height_mm);
2125 if row.use_spacing {
2126 let pad_along_row = match dir {
2127 PadRowDirection::Horizontal => pad_width_mm,
2128 PadRowDirection::Vertical => pad_height_mm,
2129 };
2130 let effective_pitch = pitch_mm + pad_along_row;
2131 builder.add_th_pad_row(
2132 row.count,
2133 effective_pitch,
2134 pad_diameter,
2135 hole_mm,
2136 x_mm,
2137 y_mm,
2138 dir,
2139 row.start,
2140 shape,
2141 );
2142 } else {
2143 builder.add_th_pad_row(
2144 row.count,
2145 pitch_mm,
2146 pad_diameter,
2147 hole_mm,
2148 x_mm,
2149 y_mm,
2150 dir,
2151 row.start,
2152 shape,
2153 );
2154 }
2155 } else if row.use_spacing {
2156 builder.add_pad_row_with_spacing(
2157 row.count,
2158 pitch_mm,
2159 pad_width_mm,
2160 pad_height_mm,
2161 x_mm,
2162 y_mm,
2163 dir,
2164 row.start,
2165 shape,
2166 );
2167 } else {
2168 builder.add_pad_row(
2169 row.count,
2170 pitch_mm,
2171 pad_width_mm,
2172 pad_height_mm,
2173 x_mm,
2174 y_mm,
2175 dir,
2176 row.start,
2177 shape,
2178 );
2179 }
2180 }
2181
2182 for dual in &footprint_def.dual_rows {
2184 let pitch_mm = parse_unit_value_or_mm(&dual.pitch)?;
2185 let row_spacing_mm = parse_unit_value_or_mm(&dual.row_spacing)?;
2186 let shape = parse_pad_shape(&dual.shape)?;
2187
2188 if let Some(ref hole_str) = dual.hole {
2189 let hole_mm = parse_unit_value_or_mm(hole_str)?;
2191 let pad_dia_mm = if let Some(ref d) = dual.pad_diameter {
2192 parse_unit_value_or_mm(d)?
2193 } else if let Some(ref w) = dual.pad_width {
2194 parse_unit_value_or_mm(w)?
2195 } else {
2196 return Err("Through-hole dual_row requires pad_diameter or pad_width"
2197 .to_string()
2198 .into());
2199 };
2200 builder.add_dual_row_th(
2201 dual.pads_per_side,
2202 pitch_mm,
2203 row_spacing_mm,
2204 pad_dia_mm,
2205 hole_mm,
2206 shape,
2207 );
2208 } else {
2209 let pad_width_mm = dual
2211 .pad_width
2212 .as_ref()
2213 .ok_or("SMD dual_row requires pad_width")?;
2214 let pad_height_mm = dual
2215 .pad_height
2216 .as_ref()
2217 .ok_or("SMD dual_row requires pad_height")?;
2218 let pad_width_mm = parse_unit_value_or_mm(pad_width_mm)?;
2219 let pad_height_mm = parse_unit_value_or_mm(pad_height_mm)?;
2220 builder.add_dual_row_smd(
2221 dual.pads_per_side,
2222 pitch_mm,
2223 row_spacing_mm,
2224 pad_width_mm,
2225 pad_height_mm,
2226 shape,
2227 );
2228 }
2229 }
2230
2231 for quad in &footprint_def.quad_pads {
2233 let pitch_mm = parse_unit_value_or_mm(&quad.pitch)?;
2234 let span_mm = parse_unit_value_or_mm(&quad.span)?;
2235 let pad_width_mm = parse_unit_value_or_mm(&quad.pad_width)?;
2236 let pad_height_mm = parse_unit_value_or_mm(&quad.pad_height)?;
2237 let shape = parse_pad_shape(&quad.shape)?;
2238 builder.add_quad_pads_smd(
2239 quad.pads_per_side,
2240 pitch_mm,
2241 span_mm,
2242 pad_width_mm,
2243 pad_height_mm,
2244 shape,
2245 );
2246 }
2247
2248 for grid in &footprint_def.pad_grids {
2250 let pitch_mm = parse_unit_value_or_mm(&grid.pitch)?;
2251 let pad_diameter_mm = parse_unit_value_or_mm(&grid.pad_diameter)?;
2252 let skip_center_mm = if grid.skip_center.is_empty() {
2253 0.0
2254 } else {
2255 parse_unit_value_or_mm(&grid.skip_center)?
2256 };
2257 let shape = parse_pad_shape(&grid.shape)?;
2258 builder.add_pad_grid(
2259 grid.rows,
2260 grid.cols,
2261 pitch_mm,
2262 pad_diameter_mm,
2263 shape,
2264 skip_center_mm,
2265 );
2266 }
2267
2268 for pad in &footprint_def.pads {
2272 let shape = parse_pad_shape(&pad.shape)?;
2273
2274 if pad.hole > 0.0 {
2275 builder.add_th_pad(
2276 &pad.designator,
2277 pad.x,
2278 pad.y,
2279 pad.width.max(pad.height),
2280 pad.hole,
2281 shape,
2282 );
2283 } else {
2284 builder.add_smd_pad(&pad.designator, pad.x, pad.y, pad.width, pad.height, shape);
2285 }
2286 }
2287
2288 for line in &footprint_def.lines {
2290 builder.add_silkscreen_line(line.x1, line.y1, line.x2, line.y2, line.width);
2291 }
2292
2293 for arc in &footprint_def.arcs {
2295 builder.add_silkscreen_arc(
2296 arc.x,
2297 arc.y,
2298 arc.radius,
2299 arc.start_angle,
2300 arc.end_angle,
2301 arc.width,
2302 );
2303 }
2304
2305 let mut det = ();
2306 let mut component = builder.build_deterministic(&mut det);
2307
2308 for text_def in &footprint_def.texts {
2310 let layer = parse_pcb_layer(&text_def.layer)?;
2311
2312 let text = PcbText::new(
2313 text_def.x,
2314 text_def.y,
2315 &text_def.text,
2316 text_def.height,
2317 text_def.stroke_width,
2318 text_def.rotation,
2319 text_def.mirrored,
2320 layer,
2321 );
2322 component.add_primitive(PcbRecord::Text(text));
2323 }
2324
2325 let pad_count = component.pad_count();
2326 let line_count = footprint_def.lines.len();
2327 let arc_count = footprint_def.arcs.len();
2328 let text_count = footprint_def.texts.len();
2329
2330 lib.components.push(component);
2331 save_pcblib(path, &lib)?;
2332
2333 let mut parts = vec![format!("{} pads", pad_count)];
2335 if line_count > 0 {
2336 parts.push(format!("{} lines", line_count));
2337 }
2338 if arc_count > 0 {
2339 parts.push(format!("{} arcs", arc_count));
2340 }
2341 if text_count > 0 {
2342 parts.push(format!("{} texts", text_count));
2343 }
2344
2345 println!(
2346 "Added footprint '{}' with {} to {}",
2347 footprint_def.name,
2348 parts.join(", "),
2349 path.display()
2350 );
2351
2352 Ok(())
2353}
2354
2355fn parse_pcb_layer(s: &str) -> Result<Layer, String> {
2356 match s.to_lowercase().replace('_', "").as_str() {
2357 "topoverlay" | "silkscreen" | "top_overlay" => Ok(Layer::TOP_OVERLAY),
2358 "bottomoverlay" | "bottom_overlay" => Ok(Layer::BOTTOM_OVERLAY),
2359 "top" | "toplayer" => Ok(Layer::TOP_LAYER),
2360 "bottom" | "bottomlayer" => Ok(Layer::BOTTOM_LAYER),
2361 _ => Err(format!(
2362 "Unknown layer: {}. Use: top_overlay, bottom_overlay, top, bottom",
2363 s
2364 )),
2365 }
2366}
2367
2368fn parse_pad_shape(s: &str) -> Result<PcbPadShape, String> {
2369 match s.to_lowercase().as_str() {
2370 "round" => Ok(PcbPadShape::Round),
2371 "rectangular" | "rect" => Ok(PcbPadShape::Rectangular),
2372 "rounded_rect" | "roundedrect" | "rounded_rectangle" => Ok(PcbPadShape::RoundedRectangle),
2373 "octagonal" | "oct" => Ok(PcbPadShape::Octagonal),
2374 _ => Err(format!(
2375 "Unknown pad shape: {}. Use: round, rectangular, rounded_rect, octagonal",
2376 s
2377 )),
2378 }
2379}
2380
2381fn parse_unit_value(s: &str) -> Result<f64, String> {
2387 let (coord, _unit) =
2388 Unit::parse_with_unit(s).map_err(|e| format!("Invalid value '{}': {:?}", s, e))?;
2389 Ok(coord.to_mms())
2390}
2391
2392fn parse_unit_value_or_mm(s: &str) -> Result<f64, String> {
2395 let s = s.trim();
2396
2397 if let Ok((coord, _unit)) = Unit::parse_with_unit(s) {
2399 return Ok(coord.to_mms());
2400 }
2401
2402 s.parse::<f64>().map_err(|_| {
2404 format!(
2405 "Invalid value '{}': expected number with optional unit (e.g., '0.5mm', '50mil')",
2406 s
2407 )
2408 })
2409}
2410
2411#[allow(clippy::too_many_arguments)]
2413pub fn cmd_add_pad_row(
2414 path: &Path,
2415 footprint: &str,
2416 count: usize,
2417 pitch: &str,
2418 pad_width: &str,
2419 pad_height: &str,
2420 direction: &str,
2421 start: u32,
2422 x: &str,
2423 y: &str,
2424 shape_str: &str,
2425 hole: &str,
2426 use_spacing: bool,
2427) -> Result<(), Box<dyn std::error::Error>> {
2428 let mut lib = open_pcblib(path)?;
2429
2430 let component = lib
2431 .components
2432 .iter_mut()
2433 .find(|c| c.pattern == footprint)
2434 .ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
2435
2436 let pitch_mm = parse_unit_value(pitch)?;
2438 let pad_width_mm = parse_unit_value(pad_width)?;
2439 let pad_height_mm = parse_unit_value(pad_height)?;
2440 let x_mm = parse_unit_value(x)?;
2441 let y_mm = parse_unit_value(y)?;
2442 let hole_mm = parse_unit_value(hole)?;
2443
2444 let dir = PadRowDirection::try_parse(direction).ok_or_else(|| {
2445 format!(
2446 "Invalid direction '{}'. Use: horizontal (h/x) or vertical (v/y)",
2447 direction
2448 )
2449 })?;
2450
2451 let shape = parse_pad_shape(shape_str)?;
2452
2453 let mut builder = FootprintBuilder::new(footprint);
2455
2456 if hole_mm > 0.0 {
2457 let pad_diameter = pad_width_mm.max(pad_height_mm);
2459 if use_spacing {
2460 let pad_along_row = match dir {
2462 PadRowDirection::Horizontal => pad_width_mm,
2463 PadRowDirection::Vertical => pad_height_mm,
2464 };
2465 let effective_pitch = pitch_mm + pad_along_row;
2466 builder.add_th_pad_row(
2467 count,
2468 effective_pitch,
2469 pad_diameter,
2470 hole_mm,
2471 x_mm,
2472 y_mm,
2473 dir,
2474 start,
2475 shape,
2476 );
2477 } else {
2478 builder.add_th_pad_row(
2479 count,
2480 pitch_mm,
2481 pad_diameter,
2482 hole_mm,
2483 x_mm,
2484 y_mm,
2485 dir,
2486 start,
2487 shape,
2488 );
2489 }
2490 } else {
2491 if use_spacing {
2493 builder.add_pad_row_with_spacing(
2494 count,
2495 pitch_mm,
2496 pad_width_mm,
2497 pad_height_mm,
2498 x_mm,
2499 y_mm,
2500 dir,
2501 start,
2502 shape,
2503 );
2504 } else {
2505 builder.add_pad_row(
2506 count,
2507 pitch_mm,
2508 pad_width_mm,
2509 pad_height_mm,
2510 x_mm,
2511 y_mm,
2512 dir,
2513 start,
2514 shape,
2515 );
2516 }
2517 }
2518
2519 let mut det = ();
2521 let temp = builder.build_deterministic(&mut det);
2522 for prim in temp.primitives {
2523 component.add_primitive(prim);
2524 }
2525
2526 save_pcblib(path, &lib)?;
2527
2528 let term = if use_spacing { "spacing" } else { "pitch" };
2529 println!(
2530 "Added {} pads to '{}' ({} {} {}, direction: {})",
2531 count,
2532 footprint,
2533 pitch,
2534 term,
2535 if hole_mm > 0.0 { "through-hole" } else { "SMD" },
2536 direction
2537 );
2538
2539 Ok(())
2540}
2541
2542#[allow(clippy::too_many_arguments)]
2544pub fn cmd_add_dual_row(
2545 path: &Path,
2546 footprint: &str,
2547 pads_per_side: usize,
2548 pitch: &str,
2549 row_spacing: &str,
2550 pad_width: Option<&str>,
2551 pad_height: Option<&str>,
2552 pad_diameter: Option<&str>,
2553 hole: Option<&str>,
2554 shape_str: &str,
2555) -> Result<(), Box<dyn std::error::Error>> {
2556 let mut lib = open_pcblib(path)?;
2557
2558 let component = lib
2559 .components
2560 .iter_mut()
2561 .find(|c| c.pattern == footprint)
2562 .ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
2563
2564 let pitch_mm = parse_unit_value(pitch)?;
2565 let row_spacing_mm = parse_unit_value(row_spacing)?;
2566 let shape = parse_pad_shape(shape_str)?;
2567
2568 let mut builder = FootprintBuilder::new(footprint);
2569
2570 if let Some(hole_str) = hole {
2572 let hole_mm = parse_unit_value(hole_str)?;
2574 let pad_dia_mm = if let Some(d) = pad_diameter {
2575 parse_unit_value(d)?
2576 } else if let Some(w) = pad_width {
2577 parse_unit_value(w)?
2578 } else {
2579 return Err("Through-hole pads require --pad-diameter or --pad-width"
2580 .to_string()
2581 .into());
2582 };
2583
2584 builder.add_dual_row_th(
2585 pads_per_side,
2586 pitch_mm,
2587 row_spacing_mm,
2588 pad_dia_mm,
2589 hole_mm,
2590 shape,
2591 );
2592 } else {
2593 let pad_w = pad_width.ok_or("SMD pads require --pad-width")?;
2595 let pad_h = pad_height.ok_or("SMD pads require --pad-height")?;
2596 let pad_width_mm = parse_unit_value(pad_w)?;
2597 let pad_height_mm = parse_unit_value(pad_h)?;
2598
2599 builder.add_dual_row_smd(
2600 pads_per_side,
2601 pitch_mm,
2602 row_spacing_mm,
2603 pad_width_mm,
2604 pad_height_mm,
2605 shape,
2606 );
2607 }
2608
2609 let mut det = ();
2611 let temp = builder.build_deterministic(&mut det);
2612 for prim in temp.primitives {
2613 component.add_primitive(prim);
2614 }
2615
2616 save_pcblib(path, &lib)?;
2617
2618 let total_pads = pads_per_side * 2;
2619 let pad_type = if hole.is_some() {
2620 "through-hole"
2621 } else {
2622 "SMD"
2623 };
2624 println!(
2625 "Added dual row ({} {} pads, {} per side) to '{}' (pitch: {}, row spacing: {})",
2626 total_pads, pad_type, pads_per_side, footprint, pitch, row_spacing
2627 );
2628
2629 Ok(())
2630}
2631
2632#[allow(clippy::too_many_arguments)]
2634pub fn cmd_add_quad_pads(
2635 path: &Path,
2636 footprint: &str,
2637 pads_per_side: usize,
2638 pitch: &str,
2639 span: &str,
2640 pad_width: &str,
2641 pad_height: &str,
2642 shape_str: &str,
2643) -> Result<(), Box<dyn std::error::Error>> {
2644 let mut lib = open_pcblib(path)?;
2645
2646 let component = lib
2647 .components
2648 .iter_mut()
2649 .find(|c| c.pattern == footprint)
2650 .ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
2651
2652 let pitch_mm = parse_unit_value(pitch)?;
2653 let span_mm = parse_unit_value(span)?;
2654 let pad_width_mm = parse_unit_value(pad_width)?;
2655 let pad_height_mm = parse_unit_value(pad_height)?;
2656 let shape = parse_pad_shape(shape_str)?;
2657
2658 let mut builder = FootprintBuilder::new(footprint);
2659 builder.add_quad_pads_smd(
2660 pads_per_side,
2661 pitch_mm,
2662 span_mm,
2663 pad_width_mm,
2664 pad_height_mm,
2665 shape,
2666 );
2667
2668 let mut det = ();
2670 let temp = builder.build_deterministic(&mut det);
2671 for prim in temp.primitives {
2672 component.add_primitive(prim);
2673 }
2674
2675 save_pcblib(path, &lib)?;
2676
2677 let total_pads = pads_per_side * 4;
2678 println!(
2679 "Added quad arrangement ({} SMD pads, {} per side) to '{}' (pitch: {}, span: {})",
2680 total_pads, pads_per_side, footprint, pitch, span
2681 );
2682
2683 Ok(())
2684}
2685
2686#[allow(clippy::too_many_arguments)]
2688pub fn cmd_add_pad_grid(
2689 path: &Path,
2690 footprint: &str,
2691 rows: usize,
2692 cols: usize,
2693 pitch: &str,
2694 pad_diameter: &str,
2695 shape_str: &str,
2696 skip_center: &str,
2697) -> Result<(), Box<dyn std::error::Error>> {
2698 let mut lib = open_pcblib(path)?;
2699
2700 let component = lib
2701 .components
2702 .iter_mut()
2703 .find(|c| c.pattern == footprint)
2704 .ok_or_else(|| format!("Footprint '{}' not found", footprint))?;
2705
2706 let pitch_mm = parse_unit_value(pitch)?;
2707 let pad_diameter_mm = parse_unit_value(pad_diameter)?;
2708 let skip_center_mm = parse_unit_value(skip_center)?;
2709 let shape = parse_pad_shape(shape_str)?;
2710
2711 let mut builder = FootprintBuilder::new(footprint);
2712 builder.add_pad_grid(rows, cols, pitch_mm, pad_diameter_mm, shape, skip_center_mm);
2713
2714 let mut det = ();
2716 let temp = builder.build_deterministic(&mut det);
2717 let pad_count = temp.primitives.len();
2718 for prim in temp.primitives {
2719 component.add_primitive(prim);
2720 }
2721
2722 save_pcblib(path, &lib)?;
2723
2724 let max_pads = rows * cols;
2725 let skipped = max_pads - pad_count;
2726 if skipped > 0 {
2727 println!(
2728 "Added {}x{} grid ({} pads, {} skipped in center) to '{}' (pitch: {})",
2729 rows, cols, pad_count, skipped, footprint, pitch
2730 );
2731 } else {
2732 println!(
2733 "Added {}x{} grid ({} pads) to '{}' (pitch: {})",
2734 rows, cols, pad_count, footprint, pitch
2735 );
2736 }
2737
2738 Ok(())
2739}