Skip to main content

altium_format/footprint/
builder.rs

1//! Footprint builder for programmatic footprint creation.
2
3use 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/// Direction for a row of pads.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum PadRowDirection {
12    /// Pads arranged horizontally (along X axis).
13    Horizontal,
14    /// Pads arranged vertically (along Y axis).
15    Vertical,
16}
17
18impl PadRowDirection {
19    /// Parse from string (accepts "horizontal", "h", "x", "vertical", "v", "y").
20    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/// Builder for creating PCB footprints.
30#[derive(Debug)]
31pub struct FootprintBuilder {
32    /// Footprint pattern name.
33    name: String,
34    /// Footprint description.
35    description: String,
36    /// Component height (for 3D).
37    height: Coord,
38    /// Primitives in the footprint.
39    primitives: Vec<PcbRecord>,
40    /// Next pad designator number.
41    next_pad_num: u32,
42}
43
44impl FootprintBuilder {
45    /// Create a new footprint builder.
46    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    /// Set the footprint description.
57    pub fn description(mut self, desc: impl Into<String>) -> Self {
58        self.description = desc.into();
59        self
60    }
61
62    /// Set the component height.
63    pub fn height_mm(mut self, height: f64) -> Self {
64        self.height = Coord::from_mms(height);
65        self
66    }
67
68    /// Add an SMD pad.
69    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    /// Add an SMD pad with auto-generated designator.
92    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    /// Add a through-hole pad.
106    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    /// Add a through-hole pad with auto-generated designator.
128    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    /// Add a rectangular through-hole pad (for pin 1 marking, etc.).
149    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        // Set different X/Y sizes for rectangular pad
167        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    /// Add a silkscreen line (track on top overlay).
176    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    /// Add a silkscreen rectangle outline.
200    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        // Draw four lines for rectangle
212        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    /// Add a silkscreen arc.
244    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    /// Add a silkscreen circle.
270    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    /// Add pin 1 indicator (small dot on silkscreen).
281    pub fn add_pin1_indicator(&mut self, x_mm: f64, y_mm: f64, radius_mm: f64) -> &mut Self {
282        // Small filled circle - use a thick arc
283        self.add_silkscreen_circle(x_mm, y_mm, radius_mm / 2.0, radius_mm)
284    }
285
286    // ═══════════════════════════════════════════════════════════════════════════
287    // HIGH-LEVEL PAD CREATION (matches datasheet terminology)
288    // ═══════════════════════════════════════════════════════════════════════════
289
290    /// Add a row of SMD pads with specified pitch (center-to-center distance).
291    ///
292    /// This is the most fundamental high-level operation - creates equally-spaced
293    /// pads in a line, like you'd see on one side of a SOIC or connector.
294    ///
295    /// # Arguments
296    /// * `count` - Number of pads to create
297    /// * `pitch_mm` - Center-to-center distance between adjacent pads
298    /// * `pad_width_mm` - Width of each pad (perpendicular to row)
299    /// * `pad_height_mm` - Height of each pad (along row direction)
300    /// * `start_x_mm` - X position of first pad center
301    /// * `start_y_mm` - Y position of first pad center
302    /// * `direction` - "horizontal" (pads extend in +X) or "vertical" (pads extend in +Y)
303    /// * `start_designator` - First pad number (subsequent pads increment from this)
304    /// * `shape` - Pad shape
305    #[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    /// Add a row of through-hole pads with specified pitch.
331    #[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    /// Add dual rows of SMD pads (like SOIC, SOP, TSSOP packages).
357    ///
358    /// Creates two parallel rows of pads, numbered sequentially down one side
359    /// then up the other (standard IC numbering).
360    ///
361    /// # Arguments
362    /// * `pads_per_side` - Number of pads on each side
363    /// * `pitch_mm` - Center-to-center distance between adjacent pads (along each row)
364    /// * `row_spacing_mm` - Distance between row centers (lead span / center-to-center)
365    /// * `pad_width_mm` - Pad width (perpendicular to package body)
366    /// * `pad_height_mm` - Pad height (along package body)
367    /// * `shape` - Pad shape
368    ///
369    /// The package is centered at origin. Left row is pads 1 to N, right row is N+1 to 2N
370    /// (numbered bottom-to-top on left, top-to-bottom on right).
371    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        // Left row (pins 1 to N, bottom to top)
385        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        // Right row (pins N+1 to 2N, top to bottom)
399        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    /// Add dual rows of through-hole pads (like DIP packages).
416    ///
417    /// Same numbering as `add_dual_row_smd`.
418    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        // Left row (pins 1 to N, bottom to top)
432        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        // Right row (pins N+1 to 2N, top to bottom)
446        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    /// Add quad arrangement of SMD pads (like QFP, LQFP, TQFP packages).
463    ///
464    /// Creates four rows of pads around a square/rectangular body.
465    /// Numbering starts at bottom-left corner of left side, goes counter-clockwise.
466    ///
467    /// # Arguments
468    /// * `pads_per_side` - Number of pads on each side
469    /// * `pitch_mm` - Center-to-center distance between adjacent pads
470    /// * `span_mm` - Distance between opposite row centers (lead span)
471    /// * `pad_width_mm` - Pad width (perpendicular to body edge)
472    /// * `pad_height_mm` - Pad height (along body edge)
473    /// * `shape` - Pad shape
474    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        // Left side (bottom to top)
489        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        // Bottom side (left to right)
503        for i in 0..pads_per_side {
504            let x = start_offset + i as f64 * pitch_mm;
505            // Rotated 90 degrees, so swap width/height
506            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        // Right side (bottom to top) - numbered in reverse
518        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        // Top side (right to left) - numbered in reverse
532        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    /// Add a grid of SMD pads (like BGA, LGA packages).
549    ///
550    /// Creates a matrix of pads with alphanumeric designators (A1, A2, ..., B1, B2, ...).
551    ///
552    /// # Arguments
553    /// * `rows` - Number of rows (letters A, B, C, ...)
554    /// * `cols` - Number of columns (numbers 1, 2, 3, ...)
555    /// * `pitch_mm` - Center-to-center distance (same for X and Y)
556    /// * `pad_diameter_mm` - Pad diameter
557    /// * `shape` - Pad shape (typically Round for BGA)
558    /// * `skip_center` - If > 0, skip pads within this radius from center (for thermal pad)
559    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; // Start from top
572
573        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                // Skip if within center exclusion zone
581                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    /// Add a grid of SMD pads with separate X and Y pitches.
597    #[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    /// Add pads using "spacing" (edge-to-edge) rather than pitch (center-to-center).
628    ///
629    /// Useful when datasheets specify gap between pads rather than pitch.
630    ///
631    /// # Arguments
632    /// * `count` - Number of pads
633    /// * `spacing_mm` - Edge-to-edge distance between adjacent pads
634    /// * `pad_width_mm` - Pad width (in the direction of the row)
635    /// * `pad_height_mm` - Pad height (perpendicular to row)
636    /// * `start_x_mm`, `start_y_mm` - Position of first pad center
637    /// * `direction` - Row direction
638    /// * `start_designator` - First pad number
639    /// * `shape` - Pad shape
640    #[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        // Convert spacing to pitch: pitch = spacing + pad_dimension_along_row
654        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    /// Add a courtyard line (mechanical layer).
673    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, // Courtyard layer
684                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    /// Add a courtyard rectangle.
697    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    /// Add assembly layer outline (for fabrication drawings).
740    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, // Assembly layer
755                    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    /// Build the footprint component (non-deterministic).
798    ///
799    /// **Prefer using `build_deterministic()` for reproducible execution.**
800    #[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    /// Build component with standard UUID generation.
816    ///
817    /// Standalone library uses standard UUIDs; Cadatomic fork replaces with deterministic context.
818    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    // Internal helper methods
830
831    #[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        // SMD pad only on specified layer
847        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
916/// Extension methods for PcbComponent to support editing.
917impl PcbComponent {
918    /// Create a new empty component (non-deterministic).
919    ///
920    /// **Prefer using `new_deterministic()` for reproducible execution.**
921    #[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    /// Create new component with standard UUID generation.
937    ///
938    /// Standalone library uses standard UUIDs; Cadatomic fork replaces with deterministic context.
939    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    /// Set the description.
951    pub fn set_description(&mut self, desc: impl Into<String>) {
952        self.description = desc.into();
953    }
954
955    /// Add a primitive.
956    pub fn add_primitive(&mut self, record: PcbRecord) {
957        self.primitives.push(record);
958    }
959
960    /// Remove a primitive by index.
961    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    /// Find a pad by designator.
970    pub fn find_pad(&self, designator: &str) -> Option<&PcbPad> {
971        self.pads().find(|p| p.designator == designator)
972    }
973
974    /// Find a pad by designator (mutable).
975    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    // ═══════════════════════════════════════════════════════════════════════════
1031    // HIGH-LEVEL PAD CREATION TESTS
1032    // ═══════════════════════════════════════════════════════════════════════════
1033
1034    #[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,    // count
1040            2.54, // pitch (mm)
1041            1.5,  // pad_width (mm)
1042            0.6,  // pad_height (mm)
1043            0.0,  // start_x (mm)
1044            0.0,  // start_y (mm)
1045            PadRowDirection::Horizontal,
1046            1, // start_designator
1047            PcbPadShape::Rectangular,
1048        );
1049
1050        let footprint = builder.build_deterministic(&mut det);
1051        assert_eq!(footprint.pad_count(), 8);
1052
1053        // Verify positions (pads should be at 0, 2.54, 5.08, ... mm)
1054        let pads: Vec<_> = footprint.pads().collect();
1055        assert_eq!(pads[0].designator, "1");
1056        assert_eq!(pads[7].designator, "8");
1057
1058        // Check spacing: pad 2 should be at ~2.54mm from pad 1
1059        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, // 1mm pitch
1071            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        // Verify Y spacing
1084        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,    // pads_per_side
1096            1.27, // pitch (mm)
1097            5.3,  // row_spacing (mm)
1098            1.5,  // pad_width (mm)
1099            0.6,  // pad_height (mm)
1100            PcbPadShape::Rectangular,
1101        );
1102
1103        let footprint = builder.build_deterministic(&mut det);
1104        assert_eq!(footprint.pad_count(), 8); // 4 * 2 = 8
1105
1106        // Verify pin numbering: 1-4 on left, 5-8 on right
1107        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        // Verify row spacing: left pads at -2.65mm, right at +2.65mm
1114        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,    // pads_per_side
1125            2.54, // pitch (mm) - 100mil
1126            7.62, // row_spacing (mm) - 300mil
1127            1.6,  // pad_diameter (mm)
1128            0.9,  // hole_diameter (mm)
1129            PcbPadShape::Round,
1130        );
1131
1132        let footprint = builder.build_deterministic(&mut det);
1133        assert_eq!(footprint.pad_count(), 16); // 8 * 2 = 16
1134
1135        // Verify it's through-hole (has holes)
1136        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,  // pads_per_side
1147            0.5, // pitch (mm)
1148            9.0, // span (mm)
1149            1.5, // pad_width (mm)
1150            0.3, // pad_height (mm)
1151            PcbPadShape::Rectangular,
1152        );
1153
1154        let footprint = builder.build_deterministic(&mut det);
1155        assert_eq!(footprint.pad_count(), 48); // 12 * 4 = 48
1156
1157        // Verify sequential numbering
1158        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,   // rows (A-H)
1171            8,   // cols (1-8)
1172            0.8, // pitch (mm)
1173            0.4, // pad_diameter (mm)
1174            PcbPadShape::Round,
1175            0.0, // skip_center (no center skip)
1176        );
1177
1178        let footprint = builder.build_deterministic(&mut det);
1179        assert_eq!(footprint.pad_count(), 64); // 8 * 8 = 64
1180
1181        // Verify alphanumeric designators
1182        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,   // rows
1195            6,   // cols
1196            1.0, // pitch (mm)
1197            0.5, // pad_diameter (mm)
1198            PcbPadShape::Round,
1199            1.5, // skip_center (skip pads within 1.5mm of center)
1200        );
1201
1202        let footprint = builder.build_deterministic(&mut det);
1203        // Should have less than 36 pads due to center skip
1204        assert!(footprint.pad_count() < 36);
1205        assert!(footprint.pad_count() > 30); // But not too many skipped
1206    }
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,   // count
1214            0.5, // spacing (mm) - edge-to-edge
1215            1.0, // pad_width (mm)
1216            0.5, // pad_height (mm)
1217            0.0, // start_x
1218            0.0, // start_y
1219            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        // With 0.5mm spacing and 1.0mm pad width, pitch should be 1.5mm
1228        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); // pitch = spacing + pad_width
1232    }
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}