idf_parser/
board.rs

1use crate::component_placement::{ComponentPlacement, parse_component_placement_section};
2use crate::drilled_holes::{Hole, parse_drilled_holes_section};
3use crate::headers::{BoardPanelHeader, parse_board_or_panel_header};
4use crate::notes::{Note, parse_notes_section};
5use crate::outlines::{
6    BoardPanelOutline, OtherOutline, PlacementGroupArea, PlacementKeepout, PlacementOutline,
7    RoutingKeepout, RoutingOutline, ViaKeepout, parse_board_panel_outline, parse_other_outline,
8    parse_placement_group_area, parse_placement_keepout, parse_placement_outline,
9    parse_routing_keepout, parse_routing_outline, parse_via_keepout,
10};
11use nom::Parser;
12use nom::multi::{many_m_n, many0};
13
14/// Represents a board or panel file in the IDF format.
15#[derive(Debug, PartialEq, Clone, Default, PartialOrd)]
16pub struct BoardPanel {
17    pub header: BoardPanelHeader,
18    pub outline: BoardPanelOutline,
19    pub other_outlines: Vec<OtherOutline>,
20    pub routing_outlines: Vec<RoutingOutline>,
21    pub placement_outlines: Vec<PlacementOutline>,
22    pub routing_keepouts: Vec<RoutingKeepout>,
23    pub via_keepouts: Vec<ViaKeepout>,
24    pub placement_keepouts: Vec<PlacementKeepout>,
25    pub placement_group_areas: Vec<PlacementGroupArea>,
26    pub drilled_holes: Vec<Hole>,
27    pub notes: Vec<Note>,
28    pub component_placements: Vec<ComponentPlacement>,
29}
30
31/// Parse the content of a board or panel .emn file into a Board struct.
32/// File specification: http://www.aertia.com/docs/priware/IDF_V30_Spec.pdf#page=8
33pub fn parse_board_or_panel(input: &str) -> Result<BoardPanel, nom::Err<nom::error::Error<&str>>> {
34    let (
35        remaining,
36        (
37            header,
38            outline,
39            other_outlines,
40            routing_outlines,
41            placement_outlines,
42            routing_keepouts,
43            via_keepouts,
44            placement_keepouts,
45            placement_group_areas,
46            drilled_holes,
47            wrapped_notes,
48            component_placements,
49        ),
50    ) = (
51        parse_board_or_panel_header,
52        parse_board_panel_outline,
53        // expect there to be between 0 and n sections
54        many0(parse_other_outline),
55        many0(parse_routing_outline),
56        many0(parse_placement_outline),
57        many0(parse_routing_keepout),
58        many0(parse_via_keepout),
59        many0(parse_placement_keepout),
60        many0(parse_placement_group_area),
61        // expect one section
62        parse_drilled_holes_section,
63        // expect either 0 or 1 sections
64        many_m_n(0, 1, parse_notes_section),
65        // expect one section
66        parse_component_placement_section,
67    )
68        .parse(input)?;
69
70    // Unwrap the notes section, if it exists. We expect there to be either 0 or 1 sections.
71    let notes: Vec<Note> = if wrapped_notes.len() == 1 {
72        wrapped_notes[0].clone()
73    } else if wrapped_notes.is_empty() {
74        Vec::new()
75    } else {
76        panic!(
77            "Expected either 1 or no notes sections, but found: {}",
78            wrapped_notes.len()
79        );
80    };
81
82    let board_panel = BoardPanel {
83        header,
84        outline,
85        other_outlines,
86        routing_outlines,
87        placement_outlines,
88        routing_keepouts,
89        via_keepouts,
90        placement_keepouts,
91        placement_group_areas,
92        drilled_holes,
93        notes,
94        component_placements,
95    };
96
97    // Check if there is any unparsed data remaining
98    if !remaining.is_empty() {
99        Err(nom::Err::Error(nom::error::Error::new(
100            remaining,
101            nom::error::ErrorKind::Eof,
102        )))
103    } else {
104        Ok(board_panel)
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::point::Point;
112    #[test]
113    fn test_parse_board() {
114        let input = ".HEADER
115BOARD_FILE 3.0 \"Sample File Generator\" 10/22/96.16:02:44 1
116sample_board THOU
117.END_HEADER
118.BOARD_OUTLINE MCAD
11962.0
1200 5030.5 -120.0 0.0
1211 3000.0 2350.0 360.0
122.END_BOARD_OUTLINE
123.ROUTE_OUTLINE ECAD
124ALL
1250 5112.5 150.0 0.0
1260 5112.5 150.0 0.0
127.END_ROUTE_OUTLINE
128.PLACE_OUTLINE MCAD
129TOP 1000.0
1300 5080.0 2034.9 0.0
1310 5080.0 2034.9 0.0
132.END_PLACE_OUTLINE
133.PLACE_OUTLINE UNOWNED
134BOTTOM 200.0
1350 300.0 200.0 0.0
1360 4800.0 200.0 0.0
137.END_PLACE_OUTLINE
138.ROUTE_KEEPOUT ECAD
139ALL
1400 2650.0 2350.0 0.0
1410 3100.0 2350.0 360.0
142.END_ROUTE_KEEPOUT
143.PLACE_KEEPOUT MCAD
144BOTH 0.0
1450 2650.0 2350.0 0.0
1460 3100.0 2350.0 360.0
147.END_PLACE_KEEPOUT
148.PLACE_KEEPOUT MCAD
149TOP 300.0
1500 3700.0 5000.0 0.0
1510 3700.0 5000.0 0.0
152.END_PLACE_KEEPOUT
153.DRILLED_HOLES
15430.0 1800.0 100.0 PTH J1 PIN ECAD
15520.0 2000.0 1600.0 PTH BOARD VIA ECAD
15693.0 5075.0 0.0 PTH BOARD MTG UNOWNED
15793.0 0.0 4800.0 NPTH BOARD TOOL MCAD
158.END_DRILLED_HOLES
159.NOTES
1601800.0 300.0 75.0 1700.0 \"Do not move connectors!\"
161.END_NOTES
162.PLACEMENT
163cs13_a pn-cap C1
1644000.0 1000.0 100.0 0.0 TOP PLACED
165cc1210 pn-cc1210 C2
1663000.0 3500.0 0.0 0.0 TOP PLACED
167cc1210 pn-cc1210 C3
1683200.0 1800.0 0.0 0.0 BOTTOM PLACED
169.END_PLACEMENT";
170
171        // Construct expected board
172        let header = BoardPanelHeader {
173            file_type: "BOARD_FILE".to_string(),
174            version: 3,
175            system_id: "Sample File Generator".to_string(),
176            date: "10/22/96.16:02:44".to_string(),
177            file_version: 1,
178            board_name: "sample_board".to_string(),
179            units: "THOU".to_string(),
180        };
181
182        let outline = BoardPanelOutline {
183            owner: "MCAD".to_string(),
184            thickness: 62.0,
185            outline: vec![
186                Point {
187                    loop_label: 0,
188                    x: 5030.5,
189                    y: -120.0,
190                    angle: 0.0,
191                },
192                Point {
193                    loop_label: 1,
194                    x: 3000.0,
195                    y: 2350.0,
196                    angle: 360.0,
197                },
198            ],
199        };
200
201        let routing_outlines = vec![RoutingOutline {
202            owner: "ECAD".to_string(),
203            routing_layers: "ALL".to_string(),
204            outline: vec![
205                Point {
206                    loop_label: 0,
207                    x: 5112.5,
208                    y: 150.0,
209                    angle: 0.0,
210                },
211                Point {
212                    loop_label: 0,
213                    x: 5112.5,
214                    y: 150.0,
215                    angle: 0.0,
216                },
217            ],
218        }];
219
220        let placement_outlines = vec![
221            PlacementOutline {
222                owner: "MCAD".to_string(),
223                board_side: "TOP".to_string(),
224                outline_height: 1000.0,
225                outline: vec![
226                    Point {
227                        loop_label: 0,
228                        x: 5080.0,
229                        y: 2034.9,
230                        angle: 0.0,
231                    },
232                    Point {
233                        loop_label: 0,
234                        x: 5080.0,
235                        y: 2034.9,
236                        angle: 0.0,
237                    },
238                ],
239            },
240            PlacementOutline {
241                owner: "UNOWNED".to_string(),
242                board_side: "BOTTOM".to_string(),
243                outline_height: 200.0,
244                outline: vec![
245                    Point {
246                        loop_label: 0,
247                        x: 300.0,
248                        y: 200.0,
249                        angle: 0.0,
250                    },
251                    Point {
252                        loop_label: 0,
253                        x: 4800.0,
254                        y: 200.0,
255                        angle: 0.0,
256                    },
257                ],
258            },
259        ];
260
261        let routing_keepouts = vec![RoutingKeepout {
262            owner: "ECAD".to_string(),
263            routing_layers: "ALL".to_string(),
264            outline: vec![
265                Point {
266                    loop_label: 0,
267                    x: 2650.0,
268                    y: 2350.0,
269                    angle: 0.0,
270                },
271                Point {
272                    loop_label: 0,
273                    x: 3100.0,
274                    y: 2350.0,
275                    angle: 360.0,
276                },
277            ],
278        }];
279
280        let placement_keepouts = vec![
281            PlacementKeepout {
282                owner: "MCAD".to_string(),
283                board_side: "BOTH".to_string(),
284                keepout_height: 0.0,
285                outline: vec![
286                    Point {
287                        loop_label: 0,
288                        x: 2650.0,
289                        y: 2350.0,
290                        angle: 0.0,
291                    },
292                    Point {
293                        loop_label: 0,
294                        x: 3100.0,
295                        y: 2350.0,
296                        angle: 360.0,
297                    },
298                ],
299            },
300            PlacementKeepout {
301                owner: "MCAD".to_string(),
302                board_side: "TOP".to_string(),
303                keepout_height: 300.0,
304                outline: vec![
305                    Point {
306                        loop_label: 0,
307                        x: 3700.0,
308                        y: 5000.0,
309                        angle: 0.0,
310                    },
311                    Point {
312                        loop_label: 0,
313                        x: 3700.0,
314                        y: 5000.0,
315                        angle: 0.0,
316                    },
317                ],
318            },
319        ];
320
321        let drilled_holes = vec![
322            Hole {
323                diameter: 30.0,
324                x: 1800.0,
325                y: 100.0,
326                plating_style: "PTH".to_string(),
327                associated_part: "J1".to_string(),
328                hole_type: "PIN".to_string(),
329                owner: "ECAD".to_string(),
330            },
331            Hole {
332                diameter: 20.0,
333                x: 2000.0,
334                y: 1600.0,
335                plating_style: "PTH".to_string(),
336                associated_part: "BOARD".to_string(),
337                hole_type: "VIA".to_string(),
338                owner: "ECAD".to_string(),
339            },
340            Hole {
341                diameter: 93.0,
342                x: 5075.0,
343                y: 0.0,
344                plating_style: "PTH".to_string(),
345                associated_part: "BOARD".to_string(),
346                hole_type: "MTG".to_string(),
347                owner: "UNOWNED".to_string(),
348            },
349            Hole {
350                diameter: 93.0,
351                x: 0.0,
352                y: 4800.0,
353                plating_style: "NPTH".to_string(),
354                associated_part: "BOARD".to_string(),
355                hole_type: "TOOL".to_string(),
356                owner: "MCAD".to_string(),
357            },
358        ];
359
360        let notes = vec![Note {
361            x: 1800.0,
362            y: 300.0,
363            text_height: 75.0,
364            test_string_physical_length: 1700.0,
365            text: "Do not move connectors!".to_string(),
366        }];
367
368        let component_placements = vec![
369            ComponentPlacement {
370                package_name: "cs13_a".to_string(),
371                part_number: "pn-cap".to_string(),
372                reference_designator: "C1".to_string(),
373                x: 4000.0,
374                y: 1000.0,
375                mounting_offset: 100.0,
376                rotation_angle: 0.0,
377                board_side: "TOP".to_string(),
378                placement_status: "PLACED".to_string(),
379            },
380            ComponentPlacement {
381                package_name: "cc1210".to_string(),
382                part_number: "pn-cc1210".to_string(),
383                reference_designator: "C2".to_string(),
384                x: 3000.0,
385                y: 3500.0,
386                mounting_offset: 0.0,
387                rotation_angle: 0.0,
388                board_side: "TOP".to_string(),
389                placement_status: "PLACED".to_string(),
390            },
391            ComponentPlacement {
392                package_name: "cc1210".to_string(),
393                part_number: "pn-cc1210".to_string(),
394                reference_designator: "C3".to_string(),
395                x: 3200.0,
396                y: 1800.0,
397                mounting_offset: 0.0,
398                rotation_angle: 0.0,
399                board_side: "BOTTOM".to_string(),
400                placement_status: "PLACED".to_string(),
401            },
402        ];
403
404        let expected_board = BoardPanel {
405            header,
406            outline,
407            other_outlines: vec![],
408            routing_outlines,
409            placement_outlines,
410            routing_keepouts,
411            via_keepouts: vec![],
412            placement_keepouts,
413            placement_group_areas: vec![],
414            drilled_holes,
415            notes,
416            component_placements,
417        };
418
419        let board = parse_board_or_panel(input).unwrap();
420        assert_eq!(board, expected_board);
421    }
422    #[test]
423    fn test_parse_panel() {
424        let input = ".HEADER
425PANEL_FILE 3.0 \"Sample File Generator\" 10/22/96.16:20:19 1
426sample_panel THOU
427.END_HEADER
428.PANEL_OUTLINE MCAD
42962.0
4300 0.0 0.0 0.0
4310 16000.0 0.0 0.0
432.END_PANEL_OUTLINE
433.PLACE_KEEPOUT MCAD
434BOTTOM 0.0
4350 13500.0 0.0 0.0
4360 13500.0 12000.0 0.0
4370 13500.0 0.0 0.0
438.END_PLACE_KEEPOUT
439.PLACE_KEEPOUT MCAD
440BOTTOM 0.0
4410 0.0 0.0 0.0
4420 2200.0 0.0 0.0
4430 2200.0 12000.0 0.0
4440 0.0 12000.0 0.0
4450 0.0 0.0 0.0
446.END_PLACE_KEEPOUT
447.DRILLED_HOLES
448250.0 15500.0 11500.0 NPTH PANEL TOOL MCAD
449250.0 500.0 500.0 NPTH PANEL TOOL MCAD
450.END_DRILLED_HOLES
451.PLACEMENT
452sample_board pn-board BOARD
4531700.0 3300.0 0.0 0.0 TOP MCAD
454.END_PLACEMENT";
455
456        let header = BoardPanelHeader {
457            file_type: "PANEL_FILE".to_string(),
458            version: 3,
459            system_id: "Sample File Generator".to_string(),
460            date: "10/22/96.16:20:19".to_string(),
461            file_version: 1,
462            board_name: "sample_panel".to_string(),
463            units: "THOU".to_string(),
464        };
465
466        let outline = BoardPanelOutline {
467            owner: "MCAD".to_string(),
468            thickness: 62.0,
469            outline: vec![
470                Point {
471                    loop_label: 0,
472                    x: 0.0,
473                    y: 0.0,
474                    angle: 0.0,
475                },
476                Point {
477                    loop_label: 0,
478                    x: 16000.0,
479                    y: 0.0,
480                    angle: 0.0,
481                },
482            ],
483        };
484
485        let placement_keepouts = vec![
486            PlacementKeepout {
487                owner: "MCAD".to_string(),
488                board_side: "BOTTOM".to_string(),
489                keepout_height: 0.0,
490                outline: vec![
491                    Point {
492                        loop_label: 0,
493                        x: 13500.0,
494                        y: 0.0,
495                        angle: 0.0,
496                    },
497                    Point {
498                        loop_label: 0,
499                        x: 13500.0,
500                        y: 12000.0,
501                        angle: 0.0,
502                    },
503                    Point {
504                        loop_label: 0,
505                        x: 13500.0,
506                        y: 0.0,
507                        angle: 0.0,
508                    },
509                ],
510            },
511            PlacementKeepout {
512                owner: "MCAD".to_string(),
513                board_side: "BOTTOM".to_string(),
514                keepout_height: 0.0,
515                outline: vec![
516                    Point {
517                        loop_label: 0,
518                        x: 0.0,
519                        y: 0.0,
520                        angle: 0.0,
521                    },
522                    Point {
523                        loop_label: 0,
524                        x: 2200.0,
525                        y: 0.0,
526                        angle: 0.0,
527                    },
528                    Point {
529                        loop_label: 0,
530                        x: 2200.0,
531                        y: 12000.0,
532                        angle: 0.0,
533                    },
534                    Point {
535                        loop_label: 0,
536                        x: 0.0,
537                        y: 12000.0,
538                        angle: 0.0,
539                    },
540                    Point {
541                        loop_label: 0,
542                        x: 0.0,
543                        y: 0.0,
544                        angle: 0.0,
545                    },
546                ],
547            },
548        ];
549
550        let drilled_holes = vec![
551            Hole {
552                diameter: 250.0,
553                x: 15500.0,
554                y: 11500.0,
555                plating_style: "NPTH".to_string(),
556                associated_part: "PANEL".to_string(),
557                hole_type: "TOOL".to_string(),
558                owner: "MCAD".to_string(),
559            },
560            Hole {
561                diameter: 250.0,
562                x: 500.0,
563                y: 500.0,
564                plating_style: "NPTH".to_string(),
565                associated_part: "PANEL".to_string(),
566                hole_type: "TOOL".to_string(),
567                owner: "MCAD".to_string(),
568            },
569        ];
570
571        let component_placements = vec![ComponentPlacement {
572            package_name: "sample_board".to_string(),
573            part_number: "pn-board".to_string(),
574            reference_designator: "BOARD".to_string(),
575            x: 1700.0,
576            y: 3300.0,
577            mounting_offset: 0.0,
578            rotation_angle: 0.0,
579            board_side: "TOP".to_string(),
580            placement_status: "MCAD".to_string(),
581        }];
582
583        let expected_panel = BoardPanel {
584            header,
585            outline,
586            other_outlines: vec![],
587            routing_outlines: vec![],
588            placement_outlines: vec![],
589            routing_keepouts: vec![],
590            via_keepouts: vec![],
591            placement_keepouts,
592            placement_group_areas: vec![],
593            drilled_holes,
594            notes: vec![],
595            component_placements,
596        };
597
598        let panel = parse_board_or_panel(input).unwrap();
599        assert_eq!(panel, expected_panel);
600    }
601}