1use crate::records::pcb::{
4 PcbArc, PcbComponent, PcbFlags, PcbPad, PcbPadHoleShape, PcbPadShape, PcbPrimitiveCommon,
5 PcbRecord, PcbStackMode, PcbTrack,
6};
7use crate::types::{Coord, CoordPoint, Layer, MaskExpansion};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum PadRowDirection {
12 Horizontal,
14 Vertical,
16}
17
18impl PadRowDirection {
19 pub fn try_parse(s: &str) -> Option<Self> {
21 match s.to_lowercase().as_str() {
22 "horizontal" | "h" | "x" | "horiz" => Some(PadRowDirection::Horizontal),
23 "vertical" | "v" | "y" | "vert" => Some(PadRowDirection::Vertical),
24 _ => None,
25 }
26 }
27}
28
29#[derive(Debug)]
31pub struct FootprintBuilder {
32 name: String,
34 description: String,
36 height: Coord,
38 primitives: Vec<PcbRecord>,
40 next_pad_num: u32,
42}
43
44impl FootprintBuilder {
45 pub fn new(name: impl Into<String>) -> Self {
47 Self {
48 name: name.into(),
49 description: String::new(),
50 height: Coord::default(),
51 primitives: Vec::new(),
52 next_pad_num: 1,
53 }
54 }
55
56 pub fn description(mut self, desc: impl Into<String>) -> Self {
58 self.description = desc.into();
59 self
60 }
61
62 pub fn height_mm(mut self, height: f64) -> Self {
64 self.height = Coord::from_mms(height);
65 self
66 }
67
68 pub fn add_smd_pad(
70 &mut self,
71 designator: impl Into<String>,
72 x_mm: f64,
73 y_mm: f64,
74 width_mm: f64,
75 height_mm: f64,
76 shape: PcbPadShape,
77 ) -> &mut Self {
78 let pad = self.create_smd_pad(
79 designator.into(),
80 Coord::from_mms(x_mm),
81 Coord::from_mms(y_mm),
82 Coord::from_mms(width_mm),
83 Coord::from_mms(height_mm),
84 shape,
85 Layer::TOP_LAYER,
86 );
87 self.primitives.push(PcbRecord::Pad(Box::new(pad)));
88 self
89 }
90
91 pub fn add_smd_pad_auto(
93 &mut self,
94 x_mm: f64,
95 y_mm: f64,
96 width_mm: f64,
97 height_mm: f64,
98 shape: PcbPadShape,
99 ) -> &mut Self {
100 let designator = self.next_pad_num.to_string();
101 self.next_pad_num += 1;
102 self.add_smd_pad(designator, x_mm, y_mm, width_mm, height_mm, shape)
103 }
104
105 pub fn add_th_pad(
107 &mut self,
108 designator: impl Into<String>,
109 x_mm: f64,
110 y_mm: f64,
111 pad_diameter_mm: f64,
112 hole_diameter_mm: f64,
113 shape: PcbPadShape,
114 ) -> &mut Self {
115 let pad = self.create_th_pad(
116 designator.into(),
117 Coord::from_mms(x_mm),
118 Coord::from_mms(y_mm),
119 Coord::from_mms(pad_diameter_mm),
120 Coord::from_mms(hole_diameter_mm),
121 shape,
122 );
123 self.primitives.push(PcbRecord::Pad(Box::new(pad)));
124 self
125 }
126
127 pub fn add_th_pad_auto(
129 &mut self,
130 x_mm: f64,
131 y_mm: f64,
132 pad_diameter_mm: f64,
133 hole_diameter_mm: f64,
134 shape: PcbPadShape,
135 ) -> &mut Self {
136 let designator = self.next_pad_num.to_string();
137 self.next_pad_num += 1;
138 self.add_th_pad(
139 designator,
140 x_mm,
141 y_mm,
142 pad_diameter_mm,
143 hole_diameter_mm,
144 shape,
145 )
146 }
147
148 pub fn add_th_rect_pad(
150 &mut self,
151 designator: impl Into<String>,
152 x_mm: f64,
153 y_mm: f64,
154 width_mm: f64,
155 height_mm: f64,
156 hole_diameter_mm: f64,
157 ) -> &mut Self {
158 let mut pad = self.create_th_pad(
159 designator.into(),
160 Coord::from_mms(x_mm),
161 Coord::from_mms(y_mm),
162 Coord::from_mms(width_mm.max(height_mm)),
163 Coord::from_mms(hole_diameter_mm),
164 PcbPadShape::Rectangular,
165 );
166 let size = CoordPoint::from_mms(width_mm, height_mm);
168 for i in 0..32 {
169 pad.size_layers[i] = size;
170 }
171 self.primitives.push(PcbRecord::Pad(Box::new(pad)));
172 self
173 }
174
175 pub fn add_silkscreen_line(
177 &mut self,
178 x1_mm: f64,
179 y1_mm: f64,
180 x2_mm: f64,
181 y2_mm: f64,
182 width_mm: f64,
183 ) -> &mut Self {
184 let track = PcbTrack {
185 common: PcbPrimitiveCommon {
186 layer: Layer::TOP_OVERLAY,
187 flags: PcbFlags::UNLOCKED | PcbFlags::UNKNOWN8,
188 unique_id: None,
189 },
190 start: CoordPoint::from_mms(x1_mm, y1_mm),
191 end: CoordPoint::from_mms(x2_mm, y2_mm),
192 width: Coord::from_mms(width_mm),
193 unknown: vec![0u8; 16],
194 };
195 self.primitives.push(PcbRecord::Track(track));
196 self
197 }
198
199 pub fn add_silkscreen_rect(
201 &mut self,
202 x_mm: f64,
203 y_mm: f64,
204 width_mm: f64,
205 height_mm: f64,
206 line_width_mm: f64,
207 ) -> &mut Self {
208 let half_w = width_mm / 2.0;
209 let half_h = height_mm / 2.0;
210
211 self.add_silkscreen_line(
213 x_mm - half_w,
214 y_mm - half_h,
215 x_mm + half_w,
216 y_mm - half_h,
217 line_width_mm,
218 );
219 self.add_silkscreen_line(
220 x_mm + half_w,
221 y_mm - half_h,
222 x_mm + half_w,
223 y_mm + half_h,
224 line_width_mm,
225 );
226 self.add_silkscreen_line(
227 x_mm + half_w,
228 y_mm + half_h,
229 x_mm - half_w,
230 y_mm + half_h,
231 line_width_mm,
232 );
233 self.add_silkscreen_line(
234 x_mm - half_w,
235 y_mm + half_h,
236 x_mm - half_w,
237 y_mm - half_h,
238 line_width_mm,
239 );
240 self
241 }
242
243 pub fn add_silkscreen_arc(
245 &mut self,
246 center_x_mm: f64,
247 center_y_mm: f64,
248 radius_mm: f64,
249 start_angle: f64,
250 end_angle: f64,
251 width_mm: f64,
252 ) -> &mut Self {
253 let arc = PcbArc {
254 common: PcbPrimitiveCommon {
255 layer: Layer::TOP_OVERLAY,
256 flags: PcbFlags::UNLOCKED | PcbFlags::UNKNOWN8,
257 unique_id: None,
258 },
259 location: CoordPoint::from_mms(center_x_mm, center_y_mm),
260 radius: Coord::from_mms(radius_mm),
261 start_angle,
262 end_angle,
263 width: Coord::from_mms(width_mm),
264 };
265 self.primitives.push(PcbRecord::Arc(arc));
266 self
267 }
268
269 pub fn add_silkscreen_circle(
271 &mut self,
272 center_x_mm: f64,
273 center_y_mm: f64,
274 radius_mm: f64,
275 width_mm: f64,
276 ) -> &mut Self {
277 self.add_silkscreen_arc(center_x_mm, center_y_mm, radius_mm, 0.0, 360.0, width_mm)
278 }
279
280 pub fn add_pin1_indicator(&mut self, x_mm: f64, y_mm: f64, radius_mm: f64) -> &mut Self {
282 self.add_silkscreen_circle(x_mm, y_mm, radius_mm / 2.0, radius_mm)
284 }
285
286 #[allow(clippy::too_many_arguments)]
306 pub fn add_pad_row(
307 &mut self,
308 count: usize,
309 pitch_mm: f64,
310 pad_width_mm: f64,
311 pad_height_mm: f64,
312 start_x_mm: f64,
313 start_y_mm: f64,
314 direction: PadRowDirection,
315 start_designator: u32,
316 shape: PcbPadShape,
317 ) -> &mut Self {
318 for i in 0..count {
319 let designator = (start_designator + i as u32).to_string();
320 let offset = i as f64 * pitch_mm;
321 let (x, y) = match direction {
322 PadRowDirection::Horizontal => (start_x_mm + offset, start_y_mm),
323 PadRowDirection::Vertical => (start_x_mm, start_y_mm + offset),
324 };
325 self.add_smd_pad(&designator, x, y, pad_width_mm, pad_height_mm, shape);
326 }
327 self
328 }
329
330 #[allow(clippy::too_many_arguments)]
332 pub fn add_th_pad_row(
333 &mut self,
334 count: usize,
335 pitch_mm: f64,
336 pad_diameter_mm: f64,
337 hole_diameter_mm: f64,
338 start_x_mm: f64,
339 start_y_mm: f64,
340 direction: PadRowDirection,
341 start_designator: u32,
342 shape: PcbPadShape,
343 ) -> &mut Self {
344 for i in 0..count {
345 let designator = (start_designator + i as u32).to_string();
346 let offset = i as f64 * pitch_mm;
347 let (x, y) = match direction {
348 PadRowDirection::Horizontal => (start_x_mm + offset, start_y_mm),
349 PadRowDirection::Vertical => (start_x_mm, start_y_mm + offset),
350 };
351 self.add_th_pad(&designator, x, y, pad_diameter_mm, hole_diameter_mm, shape);
352 }
353 self
354 }
355
356 pub fn add_dual_row_smd(
372 &mut self,
373 pads_per_side: usize,
374 pitch_mm: f64,
375 row_spacing_mm: f64,
376 pad_width_mm: f64,
377 pad_height_mm: f64,
378 shape: PcbPadShape,
379 ) -> &mut Self {
380 let half_span = row_spacing_mm / 2.0;
381 let row_length = (pads_per_side - 1) as f64 * pitch_mm;
382 let start_y = -row_length / 2.0;
383
384 for i in 0..pads_per_side {
386 let designator = (i + 1).to_string();
387 let y = start_y + i as f64 * pitch_mm;
388 self.add_smd_pad(
389 &designator,
390 -half_span,
391 y,
392 pad_width_mm,
393 pad_height_mm,
394 shape,
395 );
396 }
397
398 for i in 0..pads_per_side {
400 let designator = (pads_per_side + i + 1).to_string();
401 let y = start_y + (pads_per_side - 1 - i) as f64 * pitch_mm;
402 self.add_smd_pad(
403 &designator,
404 half_span,
405 y,
406 pad_width_mm,
407 pad_height_mm,
408 shape,
409 );
410 }
411
412 self
413 }
414
415 pub fn add_dual_row_th(
419 &mut self,
420 pads_per_side: usize,
421 pitch_mm: f64,
422 row_spacing_mm: f64,
423 pad_diameter_mm: f64,
424 hole_diameter_mm: f64,
425 shape: PcbPadShape,
426 ) -> &mut Self {
427 let half_span = row_spacing_mm / 2.0;
428 let row_length = (pads_per_side - 1) as f64 * pitch_mm;
429 let start_y = -row_length / 2.0;
430
431 for i in 0..pads_per_side {
433 let designator = (i + 1).to_string();
434 let y = start_y + i as f64 * pitch_mm;
435 self.add_th_pad(
436 &designator,
437 -half_span,
438 y,
439 pad_diameter_mm,
440 hole_diameter_mm,
441 shape,
442 );
443 }
444
445 for i in 0..pads_per_side {
447 let designator = (pads_per_side + i + 1).to_string();
448 let y = start_y + (pads_per_side - 1 - i) as f64 * pitch_mm;
449 self.add_th_pad(
450 &designator,
451 half_span,
452 y,
453 pad_diameter_mm,
454 hole_diameter_mm,
455 shape,
456 );
457 }
458
459 self
460 }
461
462 pub fn add_quad_pads_smd(
475 &mut self,
476 pads_per_side: usize,
477 pitch_mm: f64,
478 span_mm: f64,
479 pad_width_mm: f64,
480 pad_height_mm: f64,
481 shape: PcbPadShape,
482 ) -> &mut Self {
483 let half_span = span_mm / 2.0;
484 let row_length = (pads_per_side - 1) as f64 * pitch_mm;
485 let start_offset = -row_length / 2.0;
486 let mut pin = 1u32;
487
488 for i in 0..pads_per_side {
490 let y = start_offset + i as f64 * pitch_mm;
491 self.add_smd_pad(
492 pin.to_string(),
493 -half_span,
494 y,
495 pad_width_mm,
496 pad_height_mm,
497 shape,
498 );
499 pin += 1;
500 }
501
502 for i in 0..pads_per_side {
504 let x = start_offset + i as f64 * pitch_mm;
505 self.add_smd_pad(
507 pin.to_string(),
508 x,
509 -half_span,
510 pad_height_mm,
511 pad_width_mm,
512 shape,
513 );
514 pin += 1;
515 }
516
517 for i in 0..pads_per_side {
519 let y = start_offset + (pads_per_side - 1 - i) as f64 * pitch_mm;
520 self.add_smd_pad(
521 pin.to_string(),
522 half_span,
523 y,
524 pad_width_mm,
525 pad_height_mm,
526 shape,
527 );
528 pin += 1;
529 }
530
531 for i in 0..pads_per_side {
533 let x = start_offset + (pads_per_side - 1 - i) as f64 * pitch_mm;
534 self.add_smd_pad(
535 pin.to_string(),
536 x,
537 half_span,
538 pad_height_mm,
539 pad_width_mm,
540 shape,
541 );
542 pin += 1;
543 }
544
545 self
546 }
547
548 pub fn add_pad_grid(
560 &mut self,
561 rows: usize,
562 cols: usize,
563 pitch_mm: f64,
564 pad_diameter_mm: f64,
565 shape: PcbPadShape,
566 skip_center_mm: f64,
567 ) -> &mut Self {
568 let grid_width = (cols - 1) as f64 * pitch_mm;
569 let grid_height = (rows - 1) as f64 * pitch_mm;
570 let start_x = -grid_width / 2.0;
571 let start_y = grid_height / 2.0; for row in 0..rows {
574 let row_letter = (b'A' + row as u8) as char;
575 let y = start_y - row as f64 * pitch_mm;
576
577 for col in 0..cols {
578 let x = start_x + col as f64 * pitch_mm;
579
580 if skip_center_mm > 0.0 {
582 let dist = (x * x + y * y).sqrt();
583 if dist < skip_center_mm {
584 continue;
585 }
586 }
587
588 let designator = format!("{}{}", row_letter, col + 1);
589 self.add_smd_pad(&designator, x, y, pad_diameter_mm, pad_diameter_mm, shape);
590 }
591 }
592
593 self
594 }
595
596 #[allow(clippy::too_many_arguments)]
598 pub fn add_pad_grid_xy(
599 &mut self,
600 rows: usize,
601 cols: usize,
602 pitch_x_mm: f64,
603 pitch_y_mm: f64,
604 pad_width_mm: f64,
605 pad_height_mm: f64,
606 shape: PcbPadShape,
607 ) -> &mut Self {
608 let grid_width = (cols - 1) as f64 * pitch_x_mm;
609 let grid_height = (rows - 1) as f64 * pitch_y_mm;
610 let start_x = -grid_width / 2.0;
611 let start_y = grid_height / 2.0;
612
613 for row in 0..rows {
614 let row_letter = (b'A' + row as u8) as char;
615 let y = start_y - row as f64 * pitch_y_mm;
616
617 for col in 0..cols {
618 let x = start_x + col as f64 * pitch_x_mm;
619 let designator = format!("{}{}", row_letter, col + 1);
620 self.add_smd_pad(&designator, x, y, pad_width_mm, pad_height_mm, shape);
621 }
622 }
623
624 self
625 }
626
627 #[allow(clippy::too_many_arguments)]
641 pub fn add_pad_row_with_spacing(
642 &mut self,
643 count: usize,
644 spacing_mm: f64,
645 pad_width_mm: f64,
646 pad_height_mm: f64,
647 start_x_mm: f64,
648 start_y_mm: f64,
649 direction: PadRowDirection,
650 start_designator: u32,
651 shape: PcbPadShape,
652 ) -> &mut Self {
653 let pad_along_row = match direction {
655 PadRowDirection::Horizontal => pad_width_mm,
656 PadRowDirection::Vertical => pad_height_mm,
657 };
658 let pitch_mm = spacing_mm + pad_along_row;
659 self.add_pad_row(
660 count,
661 pitch_mm,
662 pad_width_mm,
663 pad_height_mm,
664 start_x_mm,
665 start_y_mm,
666 direction,
667 start_designator,
668 shape,
669 )
670 }
671
672 pub fn add_courtyard_line(
674 &mut self,
675 x1_mm: f64,
676 y1_mm: f64,
677 x2_mm: f64,
678 y2_mm: f64,
679 width_mm: f64,
680 ) -> &mut Self {
681 let track = PcbTrack {
682 common: PcbPrimitiveCommon {
683 layer: Layer::MECHANICAL_15, flags: PcbFlags::UNLOCKED | PcbFlags::UNKNOWN8,
685 unique_id: None,
686 },
687 start: CoordPoint::from_mms(x1_mm, y1_mm),
688 end: CoordPoint::from_mms(x2_mm, y2_mm),
689 width: Coord::from_mms(width_mm),
690 unknown: vec![0u8; 16],
691 };
692 self.primitives.push(PcbRecord::Track(track));
693 self
694 }
695
696 pub fn add_courtyard_rect(
698 &mut self,
699 x_mm: f64,
700 y_mm: f64,
701 width_mm: f64,
702 height_mm: f64,
703 line_width_mm: f64,
704 ) -> &mut Self {
705 let half_w = width_mm / 2.0;
706 let half_h = height_mm / 2.0;
707
708 self.add_courtyard_line(
709 x_mm - half_w,
710 y_mm - half_h,
711 x_mm + half_w,
712 y_mm - half_h,
713 line_width_mm,
714 );
715 self.add_courtyard_line(
716 x_mm + half_w,
717 y_mm - half_h,
718 x_mm + half_w,
719 y_mm + half_h,
720 line_width_mm,
721 );
722 self.add_courtyard_line(
723 x_mm + half_w,
724 y_mm + half_h,
725 x_mm - half_w,
726 y_mm + half_h,
727 line_width_mm,
728 );
729 self.add_courtyard_line(
730 x_mm - half_w,
731 y_mm + half_h,
732 x_mm - half_w,
733 y_mm - half_h,
734 line_width_mm,
735 );
736 self
737 }
738
739 pub fn add_assembly_rect(
741 &mut self,
742 x_mm: f64,
743 y_mm: f64,
744 width_mm: f64,
745 height_mm: f64,
746 line_width_mm: f64,
747 ) -> &mut Self {
748 let half_w = width_mm / 2.0;
749 let half_h = height_mm / 2.0;
750
751 let add_line = |builder: &mut Self, x1: f64, y1: f64, x2: f64, y2: f64| {
752 let track = PcbTrack {
753 common: PcbPrimitiveCommon {
754 layer: Layer::MECHANICAL_13, flags: PcbFlags::UNLOCKED | PcbFlags::UNKNOWN8,
756 unique_id: None,
757 },
758 start: CoordPoint::from_mms(x1, y1),
759 end: CoordPoint::from_mms(x2, y2),
760 width: Coord::from_mms(line_width_mm),
761 unknown: vec![0u8; 16],
762 };
763 builder.primitives.push(PcbRecord::Track(track));
764 };
765
766 add_line(
767 self,
768 x_mm - half_w,
769 y_mm - half_h,
770 x_mm + half_w,
771 y_mm - half_h,
772 );
773 add_line(
774 self,
775 x_mm + half_w,
776 y_mm - half_h,
777 x_mm + half_w,
778 y_mm + half_h,
779 );
780 add_line(
781 self,
782 x_mm + half_w,
783 y_mm + half_h,
784 x_mm - half_w,
785 y_mm + half_h,
786 );
787 add_line(
788 self,
789 x_mm - half_w,
790 y_mm + half_h,
791 x_mm - half_w,
792 y_mm - half_h,
793 );
794 self
795 }
796
797 #[deprecated(
801 since = "0.1.0",
802 note = "Use build_deterministic() with a DeterminismContext for reproducible execution"
803 )]
804 pub fn build(self) -> PcbComponent {
805 PcbComponent {
806 pattern: self.name,
807 description: self.description,
808 height: self.height,
809 item_guid: uuid::Uuid::new_v4().to_string(),
810 revision_guid: uuid::Uuid::new_v4().to_string(),
811 primitives: self.primitives,
812 }
813 }
814
815 pub fn build_deterministic(self, _det: &mut ()) -> PcbComponent {
819 PcbComponent {
820 pattern: self.name,
821 description: self.description,
822 height: self.height,
823 item_guid: uuid::Uuid::new_v4().to_string(),
824 revision_guid: uuid::Uuid::new_v4().to_string(),
825 primitives: self.primitives,
826 }
827 }
828
829 #[allow(clippy::too_many_arguments)]
832 fn create_smd_pad(
833 &self,
834 designator: String,
835 x: Coord,
836 y: Coord,
837 width: Coord,
838 height: Coord,
839 shape: PcbPadShape,
840 layer: Layer,
841 ) -> PcbPad {
842 let size = CoordPoint::new(width, height);
843 let shape_layers = [shape; 32];
844 let mut size_layers = [size; 32];
845
846 let active_layer_index = layer.to_byte() as usize - 1;
848 for (index, size_layer) in size_layers.iter_mut().enumerate() {
849 if index != active_layer_index {
850 *size_layer = CoordPoint::default();
851 }
852 }
853
854 PcbPad {
855 common: PcbPrimitiveCommon {
856 layer,
857 flags: PcbFlags::UNLOCKED | PcbFlags::UNKNOWN8,
858 unique_id: None,
859 },
860 designator,
861 location: CoordPoint::new(x, y),
862 rotation: 0.0,
863 is_plated: true,
864 jumper_id: 0,
865 stack_mode: PcbStackMode::Simple,
866 hole_size: Coord::default(),
867 hole_shape: PcbPadHoleShape::Round,
868 hole_rotation: 0.0,
869 hole_slot_length: Coord::default(),
870 paste_mask_expansion: MaskExpansion::Auto,
871 solder_mask_expansion: MaskExpansion::Auto,
872 size_layers,
873 shape_layers,
874 corner_radius_percentage: [50; 32],
875 offsets_from_hole_center: [CoordPoint::default(); 32],
876 }
877 }
878
879 fn create_th_pad(
880 &self,
881 designator: String,
882 x: Coord,
883 y: Coord,
884 pad_size: Coord,
885 hole_size: Coord,
886 shape: PcbPadShape,
887 ) -> PcbPad {
888 let size = CoordPoint::new(pad_size, pad_size);
889
890 PcbPad {
891 common: PcbPrimitiveCommon {
892 layer: Layer::multi_layer(),
893 flags: PcbFlags::UNLOCKED | PcbFlags::UNKNOWN8,
894 unique_id: None,
895 },
896 designator,
897 location: CoordPoint::new(x, y),
898 rotation: 0.0,
899 is_plated: true,
900 jumper_id: 0,
901 stack_mode: PcbStackMode::Simple,
902 hole_size,
903 hole_shape: PcbPadHoleShape::Round,
904 hole_rotation: 0.0,
905 hole_slot_length: Coord::default(),
906 paste_mask_expansion: MaskExpansion::Auto,
907 solder_mask_expansion: MaskExpansion::Auto,
908 size_layers: [size; 32],
909 shape_layers: [shape; 32],
910 corner_radius_percentage: [50; 32],
911 offsets_from_hole_center: [CoordPoint::default(); 32],
912 }
913 }
914}
915
916impl PcbComponent {
918 #[deprecated(
922 since = "0.1.0",
923 note = "Use new_deterministic() with a DeterminismContext for reproducible execution"
924 )]
925 pub fn new(pattern: impl Into<String>) -> Self {
926 Self {
927 pattern: pattern.into(),
928 description: String::new(),
929 height: Coord::default(),
930 item_guid: uuid::Uuid::new_v4().to_string(),
931 revision_guid: uuid::Uuid::new_v4().to_string(),
932 primitives: Vec::new(),
933 }
934 }
935
936 pub fn new_deterministic(pattern: impl Into<String>, _det: &mut ()) -> Self {
940 Self {
941 pattern: pattern.into(),
942 description: String::new(),
943 height: Coord::default(),
944 item_guid: uuid::Uuid::new_v4().to_string(),
945 revision_guid: uuid::Uuid::new_v4().to_string(),
946 primitives: Vec::new(),
947 }
948 }
949
950 pub fn set_description(&mut self, desc: impl Into<String>) {
952 self.description = desc.into();
953 }
954
955 pub fn add_primitive(&mut self, record: PcbRecord) {
957 self.primitives.push(record);
958 }
959
960 pub fn remove_primitive(&mut self, index: usize) -> Option<PcbRecord> {
962 if index < self.primitives.len() {
963 Some(self.primitives.remove(index))
964 } else {
965 None
966 }
967 }
968
969 pub fn find_pad(&self, designator: &str) -> Option<&PcbPad> {
971 self.pads().find(|p| p.designator == designator)
972 }
973
974 pub fn find_pad_mut(&mut self, designator: &str) -> Option<&mut PcbPad> {
976 for prim in &mut self.primitives {
977 if let PcbRecord::Pad(pad) = prim {
978 if pad.designator == designator {
979 return Some(pad);
980 }
981 }
982 }
983 None
984 }
985}
986
987#[cfg(test)]
988mod tests {
989 use super::*;
990
991 #[test]
992 fn test_footprint_builder_basic() {
993 let mut det = ();
994 let footprint = FootprintBuilder::new("TEST-FOOTPRINT")
995 .description("Test footprint")
996 .height_mm(1.0)
997 .build_deterministic(&mut det);
998
999 assert_eq!(footprint.pattern, "TEST-FOOTPRINT");
1000 assert_eq!(footprint.description, "Test footprint");
1001 }
1002
1003 #[test]
1004 fn test_footprint_builder_smd_pads() {
1005 let mut det = ();
1006 let mut builder = FootprintBuilder::new("SOT23");
1007 builder
1008 .add_smd_pad("1", -0.95, -1.0, 0.6, 0.7, PcbPadShape::Rectangular)
1009 .add_smd_pad("2", 0.95, -1.0, 0.6, 0.7, PcbPadShape::Rectangular)
1010 .add_smd_pad("3", 0.0, 1.0, 0.6, 0.7, PcbPadShape::Rectangular);
1011
1012 let footprint = builder.build_deterministic(&mut det);
1013 assert_eq!(footprint.pad_count(), 3);
1014 }
1015
1016 #[test]
1017 fn test_footprint_builder_th_pads() {
1018 let mut det = ();
1019 let mut builder = FootprintBuilder::new("DIP8");
1020 for i in 0..8 {
1021 let x = if i < 4 { -3.81 } else { 3.81 };
1022 let y = (i % 4) as f64 * 2.54 - 3.81;
1023 builder.add_th_pad_auto(x, y, 1.6, 0.9, PcbPadShape::Round);
1024 }
1025
1026 let footprint = builder.build_deterministic(&mut det);
1027 assert_eq!(footprint.pad_count(), 8);
1028 }
1029
1030 #[test]
1035 fn test_pad_row_horizontal() {
1036 let mut det = ();
1037 let mut builder = FootprintBuilder::new("CONN-8");
1038 builder.add_pad_row(
1039 8, 2.54, 1.5, 0.6, 0.0, 0.0, PadRowDirection::Horizontal,
1046 1, PcbPadShape::Rectangular,
1048 );
1049
1050 let footprint = builder.build_deterministic(&mut det);
1051 assert_eq!(footprint.pad_count(), 8);
1052
1053 let pads: Vec<_> = footprint.pads().collect();
1055 assert_eq!(pads[0].designator, "1");
1056 assert_eq!(pads[7].designator, "8");
1057
1058 let x1 = pads[0].location.x.to_mms();
1060 let x2 = pads[1].location.x.to_mms();
1061 assert!((x2 - x1 - 2.54).abs() < 0.01);
1062 }
1063
1064 #[test]
1065 fn test_pad_row_vertical() {
1066 let mut det = ();
1067 let mut builder = FootprintBuilder::new("VERT-4");
1068 builder.add_pad_row(
1069 4,
1070 1.0, 0.5,
1072 0.5,
1073 0.0,
1074 0.0,
1075 PadRowDirection::Vertical,
1076 1,
1077 PcbPadShape::Round,
1078 );
1079
1080 let footprint = builder.build_deterministic(&mut det);
1081 assert_eq!(footprint.pad_count(), 4);
1082
1083 let pads: Vec<_> = footprint.pads().collect();
1085 let y1 = pads[0].location.y.to_mms();
1086 let y2 = pads[1].location.y.to_mms();
1087 assert!((y2 - y1 - 1.0).abs() < 0.01);
1088 }
1089
1090 #[test]
1091 fn test_dual_row_smd() {
1092 let mut det = ();
1093 let mut builder = FootprintBuilder::new("SOIC-8");
1094 builder.add_dual_row_smd(
1095 4, 1.27, 5.3, 1.5, 0.6, PcbPadShape::Rectangular,
1101 );
1102
1103 let footprint = builder.build_deterministic(&mut det);
1104 assert_eq!(footprint.pad_count(), 8); let pads: Vec<_> = footprint.pads().collect();
1108 assert_eq!(pads[0].designator, "1");
1109 assert_eq!(pads[3].designator, "4");
1110 assert_eq!(pads[4].designator, "5");
1111 assert_eq!(pads[7].designator, "8");
1112
1113 let left_x = pads[0].location.x.to_mms();
1115 let right_x = pads[4].location.x.to_mms();
1116 assert!((right_x - left_x - 5.3).abs() < 0.01);
1117 }
1118
1119 #[test]
1120 fn test_dual_row_th() {
1121 let mut det = ();
1122 let mut builder = FootprintBuilder::new("DIP-16");
1123 builder.add_dual_row_th(
1124 8, 2.54, 7.62, 1.6, 0.9, PcbPadShape::Round,
1130 );
1131
1132 let footprint = builder.build_deterministic(&mut det);
1133 assert_eq!(footprint.pad_count(), 16); let pads: Vec<_> = footprint.pads().collect();
1137 assert!(pads[0].has_hole());
1138 assert!((pads[0].hole_size.to_mms() - 0.9).abs() < 0.01);
1139 }
1140
1141 #[test]
1142 fn test_quad_pads() {
1143 let mut det = ();
1144 let mut builder = FootprintBuilder::new("QFP-48");
1145 builder.add_quad_pads_smd(
1146 12, 0.5, 9.0, 1.5, 0.3, PcbPadShape::Rectangular,
1152 );
1153
1154 let footprint = builder.build_deterministic(&mut det);
1155 assert_eq!(footprint.pad_count(), 48); let pads: Vec<_> = footprint.pads().collect();
1159 assert_eq!(pads[0].designator, "1");
1160 assert_eq!(pads[11].designator, "12");
1161 assert_eq!(pads[12].designator, "13");
1162 assert_eq!(pads[47].designator, "48");
1163 }
1164
1165 #[test]
1166 fn test_pad_grid() {
1167 let mut det = ();
1168 let mut builder = FootprintBuilder::new("BGA-64");
1169 builder.add_pad_grid(
1170 8, 8, 0.8, 0.4, PcbPadShape::Round,
1175 0.0, );
1177
1178 let footprint = builder.build_deterministic(&mut det);
1179 assert_eq!(footprint.pad_count(), 64); let pads: Vec<_> = footprint.pads().collect();
1183 assert_eq!(pads[0].designator, "A1");
1184 assert_eq!(pads[7].designator, "A8");
1185 assert_eq!(pads[8].designator, "B1");
1186 assert_eq!(pads[63].designator, "H8");
1187 }
1188
1189 #[test]
1190 fn test_pad_grid_with_center_skip() {
1191 let mut det = ();
1192 let mut builder = FootprintBuilder::new("BGA-CENTER-SKIP");
1193 builder.add_pad_grid(
1194 6, 6, 1.0, 0.5, PcbPadShape::Round,
1199 1.5, );
1201
1202 let footprint = builder.build_deterministic(&mut det);
1203 assert!(footprint.pad_count() < 36);
1205 assert!(footprint.pad_count() > 30); }
1207
1208 #[test]
1209 fn test_pad_row_with_spacing() {
1210 let mut det = ();
1211 let mut builder = FootprintBuilder::new("SPACED-PADS");
1212 builder.add_pad_row_with_spacing(
1213 3, 0.5, 1.0, 0.5, 0.0, 0.0, PadRowDirection::Horizontal,
1220 1,
1221 PcbPadShape::Rectangular,
1222 );
1223
1224 let footprint = builder.build_deterministic(&mut det);
1225 assert_eq!(footprint.pad_count(), 3);
1226
1227 let pads: Vec<_> = footprint.pads().collect();
1229 let x1 = pads[0].location.x.to_mms();
1230 let x2 = pads[1].location.x.to_mms();
1231 assert!((x2 - x1 - 1.5).abs() < 0.01); }
1233
1234 #[test]
1235 fn test_pad_row_direction_parse() {
1236 assert_eq!(
1237 PadRowDirection::try_parse("horizontal"),
1238 Some(PadRowDirection::Horizontal)
1239 );
1240 assert_eq!(
1241 PadRowDirection::try_parse("h"),
1242 Some(PadRowDirection::Horizontal)
1243 );
1244 assert_eq!(
1245 PadRowDirection::try_parse("x"),
1246 Some(PadRowDirection::Horizontal)
1247 );
1248 assert_eq!(
1249 PadRowDirection::try_parse("vertical"),
1250 Some(PadRowDirection::Vertical)
1251 );
1252 assert_eq!(
1253 PadRowDirection::try_parse("v"),
1254 Some(PadRowDirection::Vertical)
1255 );
1256 assert_eq!(
1257 PadRowDirection::try_parse("y"),
1258 Some(PadRowDirection::Vertical)
1259 );
1260 assert_eq!(PadRowDirection::try_parse("invalid"), None);
1261 }
1262}