Skip to main content

altium_format/io/
pcbdoc.rs

1//! PcbDoc reader/writer for Altium PCB document files.
2//!
3//! Supports reading and writing of PCB documents including board data,
4//! components, primitives, nets, and design rules.
5
6use cfb::CompoundFile;
7use std::fs::File;
8use std::io::{Cursor, Read, Seek, SeekFrom, Write};
9use std::path::Path;
10
11use crate::dump::{DumpTree, TreeBuilder};
12use crate::error::{AltiumError, Result};
13use crate::io::reader::{read_block, read_parameters_block};
14use crate::io::writer::write_parameters_block;
15use crate::records::pcb::{
16    PcbAdvancedPlacerOptions, PcbArc, PcbClass, PcbDrcOptions, PcbFill, PcbObjectId,
17    PcbPinSwapOptions, PcbPolygon, PcbRecord, PcbRegion, PcbRule, PcbText, PcbTrack, PcbVia,
18};
19use crate::traits::FromBinary;
20use crate::types::ParameterCollection;
21
22/// A PCB document containing board data.
23#[derive(Debug, Default)]
24pub struct PcbDoc {
25    /// Board header parameters.
26    pub board_params: ParameterCollection,
27    /// Components placed on the board.
28    pub components: Vec<PcbDocComponent>,
29    /// Board primitives (not associated with components).
30    pub primitives: Vec<PcbRecord>,
31    /// Nets in the design.
32    pub nets: Vec<String>,
33    /// Design rules.
34    pub rules: Vec<PcbRule>,
35    /// Object classes (net classes, component classes, etc.).
36    pub classes: Vec<PcbClass>,
37    /// Advanced placer options.
38    pub placer_options: Option<PcbAdvancedPlacerOptions>,
39    /// Design rule checker options.
40    pub drc_options: Option<PcbDrcOptions>,
41    /// Pin swap options.
42    pub pin_swap_options: Option<PcbPinSwapOptions>,
43}
44
45/// A component placed on the board.
46#[derive(Debug, Default)]
47pub struct PcbDocComponent {
48    /// Component designator (e.g., "R1", "U1").
49    pub designator: String,
50    /// Footprint pattern name.
51    pub pattern: String,
52    /// Component comment/value.
53    pub comment: String,
54    /// Component parameters.
55    pub params: ParameterCollection,
56    /// Primitives belonging to this component.
57    pub primitives: Vec<PcbRecord>,
58}
59
60impl PcbDoc {
61    /// Open and read a PcbDoc file.
62    pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
63        let mut pcbdoc = PcbDoc::default();
64        let mut cf = CompoundFile::open(reader).map_err(|e| {
65            AltiumError::Io(std::io::Error::new(
66                std::io::ErrorKind::InvalidData,
67                e.to_string(),
68            ))
69        })?;
70
71        // Read board header/parameters
72        pcbdoc.read_board(&mut cf)?;
73
74        // Read components
75        pcbdoc.read_components(&mut cf)?;
76
77        // Read board primitives
78        pcbdoc.read_primitives(&mut cf)?;
79
80        // Read nets
81        pcbdoc.read_nets(&mut cf)?;
82
83        // Read design rules
84        pcbdoc.read_rules(&mut cf)?;
85
86        // Read classes
87        pcbdoc.read_classes(&mut cf)?;
88
89        // Read options
90        pcbdoc.read_options(&mut cf)?;
91
92        Ok(pcbdoc)
93    }
94
95    /// Open and read a PcbDoc file from a path.
96    pub fn open_file<P: AsRef<Path>>(path: P) -> Result<Self> {
97        let file = File::open(path)?;
98        Self::open(file)
99    }
100
101    /// Read the Board storage.
102    fn read_board<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
103        let data_path = "/Board6/Data";
104
105        if cf.entry(data_path).is_err() {
106            // Try alternate path
107            return Ok(());
108        }
109
110        let mut stream = cf.open_stream(data_path).map_err(|e| {
111            AltiumError::Io(std::io::Error::new(
112                std::io::ErrorKind::NotFound,
113                e.to_string(),
114            ))
115        })?;
116
117        let mut data = Vec::new();
118        stream.read_to_end(&mut data)?;
119
120        if data.is_empty() {
121            return Ok(());
122        }
123
124        let mut cursor = Cursor::new(&data);
125
126        // Read board parameters
127        self.board_params = read_parameters_block(&mut cursor)?;
128
129        Ok(())
130    }
131
132    /// Read the Components storage.
133    fn read_components<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
134        let data_path = "/Components6/Data";
135
136        if cf.entry(data_path).is_err() {
137            return Ok(());
138        }
139
140        let mut stream = cf.open_stream(data_path).map_err(|e| {
141            AltiumError::Io(std::io::Error::new(
142                std::io::ErrorKind::NotFound,
143                e.to_string(),
144            ))
145        })?;
146
147        let mut data = Vec::new();
148        stream.read_to_end(&mut data)?;
149
150        if data.is_empty() {
151            return Ok(());
152        }
153
154        let mut cursor = Cursor::new(&data);
155
156        // Read components
157        while (cursor.position() as usize) < data.len() {
158            match self.read_component_record(&mut cursor) {
159                Ok(comp) => self.components.push(comp),
160                Err(_) => break,
161            }
162        }
163
164        Ok(())
165    }
166
167    /// Read a single component record.
168    fn read_component_record<R: Read>(&self, reader: &mut R) -> Result<PcbDocComponent> {
169        let params = read_parameters_block(reader)?;
170
171        Ok(PcbDocComponent {
172            // PcbDoc uses SOURCEDESIGNATOR for the placed component's designator
173            designator: params
174                .get("SOURCEDESIGNATOR")
175                .or_else(|| params.get("DESIGNATOR"))
176                .map(|v| v.as_str().to_string())
177                .unwrap_or_default(),
178            pattern: params
179                .get("PATTERN")
180                .map(|v| v.as_str().to_string())
181                .unwrap_or_default(),
182            comment: params
183                .get("COMMENT")
184                .map(|v| v.as_str().to_string())
185                .unwrap_or_default(),
186            params,
187            primitives: Vec::new(),
188        })
189    }
190
191    /// Read board primitives (tracks, arcs, vias, etc.).
192    fn read_primitives<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
193        use byteorder::ReadBytesExt;
194
195        // Try to read from various primitive storages
196        self.read_primitive_storage(cf, "/Tracks6/Data", |cursor, _| {
197            let record_id = cursor.read_u8()?;
198            if record_id != PcbObjectId::Track.to_byte() {
199                return Err(AltiumError::InvalidRecord(format!(
200                    "Expected Track record ID (4), got {}",
201                    record_id
202                )));
203            }
204            let block = read_block(cursor)?;
205            let mut block_cursor = Cursor::new(&block);
206            <PcbTrack as FromBinary>::read_from(&mut block_cursor).map(PcbRecord::Track)
207        })?;
208
209        self.read_primitive_storage(cf, "/Arcs6/Data", |cursor, _| {
210            let record_id = cursor.read_u8()?;
211            if record_id != PcbObjectId::Arc.to_byte() {
212                return Err(AltiumError::InvalidRecord(format!(
213                    "Expected Arc record ID (1), got {}",
214                    record_id
215                )));
216            }
217            let block = read_block(cursor)?;
218            let mut block_cursor = Cursor::new(&block);
219            <PcbArc as FromBinary>::read_from(&mut block_cursor).map(PcbRecord::Arc)
220        })?;
221
222        self.read_primitive_storage(cf, "/Vias6/Data", |cursor, _| {
223            let record_id = cursor.read_u8()?;
224            if record_id != PcbObjectId::Via.to_byte() {
225                return Err(AltiumError::InvalidRecord(format!(
226                    "Expected Via record ID (3), got {}",
227                    record_id
228                )));
229            }
230            let block = read_block(cursor)?;
231            let mut block_cursor = Cursor::new(&block);
232            <PcbVia as FromBinary>::read_from(&mut block_cursor).map(PcbRecord::Via)
233        })?;
234
235        self.read_primitive_storage(cf, "/Fills6/Data", |cursor, _| {
236            let record_id = cursor.read_u8()?;
237            if record_id != PcbObjectId::Fill.to_byte() {
238                return Err(AltiumError::InvalidRecord(format!(
239                    "Expected Fill record ID (6), got {}",
240                    record_id
241                )));
242            }
243            let block = read_block(cursor)?;
244            let mut block_cursor = Cursor::new(&block);
245            <PcbFill as FromBinary>::read_from(&mut block_cursor).map(PcbRecord::Fill)
246        })?;
247
248        self.read_primitive_storage(cf, "/Regions6/Data", |cursor, _| {
249            let record_id = cursor.read_u8()?;
250            if record_id != PcbObjectId::Region.to_byte() {
251                return Err(AltiumError::InvalidRecord(format!(
252                    "Expected Region record ID (11), got {}",
253                    record_id
254                )));
255            }
256            let block = read_block(cursor)?;
257            let mut block_cursor = Cursor::new(&block);
258            <PcbRegion as FromBinary>::read_from(&mut block_cursor).map(PcbRecord::Region)
259        })?;
260
261        // Read polygons (copper pours)
262        self.read_primitive_storage(cf, "/Polygons6/Data", |cursor, _| {
263            let record_id = cursor.read_u8()?;
264            if record_id != PcbObjectId::Polygon.to_byte() {
265                return Err(AltiumError::InvalidRecord(format!(
266                    "Expected Polygon record ID (10), got {}",
267                    record_id
268                )));
269            }
270            let params = read_parameters_block(cursor)?;
271            Ok(PcbRecord::Polygon(PcbPolygon::from_params(&params)))
272        })?;
273
274        // Read texts
275        self.read_primitive_storage(cf, "/Texts6/Data", |cursor, _| {
276            let record_id = cursor.read_u8()?;
277            if record_id != PcbObjectId::Text.to_byte() {
278                return Err(AltiumError::InvalidRecord(format!(
279                    "Expected Text record ID (5), got {}",
280                    record_id
281                )));
282            }
283            let block = read_block(cursor)?;
284            let mut block_cursor = Cursor::new(&block);
285            <PcbText as FromBinary>::read_from(&mut block_cursor).map(PcbRecord::Text)
286        })?;
287
288        Ok(())
289    }
290
291    /// Read a primitive storage stream.
292    fn read_primitive_storage<R, F>(
293        &mut self,
294        cf: &mut CompoundFile<R>,
295        path: &str,
296        reader_fn: F,
297    ) -> Result<()>
298    where
299        R: Read + Seek,
300        F: Fn(&mut Cursor<&Vec<u8>>, usize) -> Result<PcbRecord>,
301    {
302        if cf.entry(path).is_err() {
303            return Ok(());
304        }
305
306        let mut stream = cf.open_stream(path).map_err(|e| {
307            AltiumError::Io(std::io::Error::new(
308                std::io::ErrorKind::NotFound,
309                e.to_string(),
310            ))
311        })?;
312
313        let mut data = Vec::new();
314        stream.read_to_end(&mut data)?;
315
316        if data.is_empty() {
317            return Ok(());
318        }
319
320        let mut cursor = Cursor::new(&data);
321        let mut index = 0;
322
323        while (cursor.position() as usize) < data.len() {
324            match reader_fn(&mut cursor, index) {
325                Ok(record) => self.primitives.push(record),
326                Err(_) => break,
327            }
328            index += 1;
329        }
330
331        Ok(())
332    }
333
334    /// Read the Nets storage.
335    fn read_nets<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
336        let data_path = "/Nets6/Data";
337
338        if cf.entry(data_path).is_err() {
339            return Ok(());
340        }
341
342        let mut stream = cf.open_stream(data_path).map_err(|e| {
343            AltiumError::Io(std::io::Error::new(
344                std::io::ErrorKind::NotFound,
345                e.to_string(),
346            ))
347        })?;
348
349        let mut data = Vec::new();
350        stream.read_to_end(&mut data)?;
351
352        if data.is_empty() {
353            return Ok(());
354        }
355
356        let mut cursor = Cursor::new(&data);
357
358        while (cursor.position() as usize) < data.len() {
359            match read_parameters_block(&mut cursor) {
360                Ok(params) => {
361                    if let Some(name) = params.get("NAME") {
362                        self.nets.push(name.as_str().to_string());
363                    }
364                }
365                Err(_) => break,
366            }
367        }
368
369        Ok(())
370    }
371
372    /// Read the Rules storage.
373    fn read_rules<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
374        let data_path = "/Rules6/Data";
375
376        if cf.entry(data_path).is_err() {
377            return Ok(());
378        }
379
380        let mut stream = cf.open_stream(data_path).map_err(|e| {
381            AltiumError::Io(std::io::Error::new(
382                std::io::ErrorKind::NotFound,
383                e.to_string(),
384            ))
385        })?;
386
387        let mut data = Vec::new();
388        stream.read_to_end(&mut data)?;
389
390        if data.is_empty() {
391            return Ok(());
392        }
393
394        let mut cursor = Cursor::new(&data);
395
396        while (cursor.position() as usize) < data.len() {
397            match PcbRule::read_from(&mut cursor) {
398                Ok(rule) => self.rules.push(rule),
399                Err(_) => break,
400            }
401        }
402
403        Ok(())
404    }
405
406    /// Read the Classes storage.
407    fn read_classes<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
408        let data_path = "/Classes6/Data";
409
410        if cf.entry(data_path).is_err() {
411            return Ok(());
412        }
413
414        let mut stream = cf.open_stream(data_path).map_err(|e| {
415            AltiumError::Io(std::io::Error::new(
416                std::io::ErrorKind::NotFound,
417                e.to_string(),
418            ))
419        })?;
420
421        let mut data = Vec::new();
422        stream.read_to_end(&mut data)?;
423
424        if data.is_empty() {
425            return Ok(());
426        }
427
428        let mut cursor = Cursor::new(&data);
429
430        while (cursor.position() as usize) < data.len() {
431            match read_parameters_block(&mut cursor) {
432                Ok(params) => {
433                    let class = PcbClass::from_params(&params);
434                    self.classes.push(class);
435                }
436                Err(_) => break,
437            }
438        }
439
440        Ok(())
441    }
442
443    /// Read various options streams.
444    fn read_options<R: Read + Seek>(&mut self, cf: &mut CompoundFile<R>) -> Result<()> {
445        // Read Advanced Placer Options
446        if let Ok(params) = Self::read_options_stream(cf, "/Advanced Placer Options6/Data") {
447            self.placer_options = Some(PcbAdvancedPlacerOptions::from_params(&params));
448        }
449
450        // Read DRC Options
451        if let Ok(params) = Self::read_options_stream(cf, "/Design Rule Checker Options6/Data") {
452            self.drc_options = Some(PcbDrcOptions::from_params(&params));
453        }
454
455        // Read Pin Swap Options
456        if let Ok(params) = Self::read_options_stream(cf, "/Pin Swap Options6/Data") {
457            self.pin_swap_options = Some(PcbPinSwapOptions::from_params(&params));
458        }
459
460        Ok(())
461    }
462
463    /// Read a single options stream as parameters.
464    fn read_options_stream<R: Read + Seek>(
465        cf: &mut CompoundFile<R>,
466        path: &str,
467    ) -> Result<ParameterCollection> {
468        if cf.entry(path).is_err() {
469            return Err(AltiumError::Parse(format!("Stream not found: {}", path)));
470        }
471
472        let mut stream = cf.open_stream(path).map_err(|e| {
473            AltiumError::Io(std::io::Error::new(
474                std::io::ErrorKind::NotFound,
475                e.to_string(),
476            ))
477        })?;
478
479        let mut data = Vec::new();
480        stream.read_to_end(&mut data)?;
481
482        if data.is_empty() {
483            return Err(AltiumError::Parse("Empty stream".to_string()));
484        }
485
486        let mut cursor = Cursor::new(&data);
487        read_parameters_block(&mut cursor)
488    }
489
490    /// Save the PcbDoc to a file path.
491    ///
492    /// This performs a read-modify-write operation: it reads the existing file,
493    /// updates the rules stream, and writes back to the same path.
494    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
495        // Read the existing file
496        let file = std::fs::OpenOptions::new()
497            .read(true)
498            .write(true)
499            .open(path.as_ref())?;
500
501        let mut cf = CompoundFile::open(file).map_err(|e| {
502            AltiumError::Io(std::io::Error::new(
503                std::io::ErrorKind::InvalidData,
504                e.to_string(),
505            ))
506        })?;
507
508        // Write rules
509        self.write_rules(&mut cf)?;
510
511        cf.flush()
512            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
513
514        Ok(())
515    }
516
517    /// Write rules to the CFB file.
518    fn write_rules<R: Read + Write + Seek>(&self, cf: &mut CompoundFile<R>) -> Result<()> {
519        let data_path = "/Rules6/Data";
520
521        // Serialize all rules to a buffer
522        let mut buffer = Vec::new();
523        for rule in &self.rules {
524            rule.write_to(&mut buffer)?;
525        }
526
527        // Check if stream exists, create if needed
528        if cf.entry(data_path).is_err() {
529            // For now, just fail if the stream doesn't exist
530            // A full implementation would create the stream
531            return Err(AltiumError::Parse(
532                "Rules6/Data stream not found".to_string(),
533            ));
534        }
535
536        // Open and truncate the stream
537        let mut stream = cf.open_stream(data_path).map_err(|e| {
538            AltiumError::Io(std::io::Error::new(
539                std::io::ErrorKind::NotFound,
540                e.to_string(),
541            ))
542        })?;
543
544        // Seek to beginning and write
545        stream.seek(SeekFrom::Start(0))?;
546        stream.write_all(&buffer)?;
547
548        // If new content is shorter, we need to truncate
549        // cfb crate's stream should handle this, but let's be safe
550        let new_len = buffer.len() as u64;
551        stream
552            .set_len(new_len)
553            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
554
555        Ok(())
556    }
557
558    /// Save board parameters to a file path.
559    ///
560    /// This performs a read-modify-write operation: it reads the existing file,
561    /// updates the Board6/Data stream, and writes back.
562    pub fn save_board_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
563        let file = std::fs::OpenOptions::new()
564            .read(true)
565            .write(true)
566            .open(path.as_ref())?;
567
568        let mut cf = CompoundFile::open(file).map_err(|e| {
569            AltiumError::Io(std::io::Error::new(
570                std::io::ErrorKind::InvalidData,
571                e.to_string(),
572            ))
573        })?;
574
575        self.write_board(&mut cf)?;
576
577        cf.flush()
578            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
579
580        Ok(())
581    }
582
583    /// Write board data to the CFB file.
584    fn write_board<R: Read + Write + Seek>(&self, cf: &mut CompoundFile<R>) -> Result<()> {
585        use crate::io::writer::write_parameters_block;
586
587        let data_path = "/Board6/Data";
588
589        // Check if stream exists
590        if cf.entry(data_path).is_err() {
591            return Err(AltiumError::Parse(
592                "Board6/Data stream not found".to_string(),
593            ));
594        }
595
596        // Serialize board params to a buffer
597        let mut buffer = Vec::new();
598        write_parameters_block(&mut buffer, &self.board_params)?;
599
600        // Open and write the stream
601        let mut stream = cf.open_stream(data_path).map_err(|e| {
602            AltiumError::Io(std::io::Error::new(
603                std::io::ErrorKind::NotFound,
604                e.to_string(),
605            ))
606        })?;
607
608        stream.seek(SeekFrom::Start(0))?;
609        stream.write_all(&buffer)?;
610
611        let new_len = buffer.len() as u64;
612        stream
613            .set_len(new_len)
614            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
615
616        Ok(())
617    }
618
619    /// Save regions (keepouts/cutouts) to a file path.
620    ///
621    /// This performs a read-modify-write operation.
622    pub fn save_regions_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
623        use crate::io::writer::write_block;
624        use crate::traits::ToBinary;
625
626        let file = std::fs::OpenOptions::new()
627            .read(true)
628            .write(true)
629            .open(path.as_ref())?;
630
631        let mut cf = CompoundFile::open(file).map_err(|e| {
632            AltiumError::Io(std::io::Error::new(
633                std::io::ErrorKind::InvalidData,
634                e.to_string(),
635            ))
636        })?;
637
638        let data_path = "/Regions6/Data";
639
640        // Check if stream exists
641        if cf.entry(data_path).is_err() {
642            return Err(AltiumError::Parse(
643                "Regions6/Data stream not found".to_string(),
644            ));
645        }
646
647        // Serialize all regions to a buffer
648        let mut buffer = Vec::new();
649        for prim in &self.primitives {
650            if let PcbRecord::Region(r) = prim {
651                let mut region_data = Vec::new();
652                r.write_to(&mut region_data)?;
653                write_block(&mut buffer, &region_data, 0)?;
654            }
655        }
656
657        // Open and write the stream
658        let mut stream = cf.open_stream(data_path).map_err(|e| {
659            AltiumError::Io(std::io::Error::new(
660                std::io::ErrorKind::NotFound,
661                e.to_string(),
662            ))
663        })?;
664
665        stream.seek(SeekFrom::Start(0))?;
666        stream.write_all(&buffer)?;
667
668        let new_len = buffer.len() as u64;
669        stream
670            .set_len(new_len)
671            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
672
673        cf.flush()
674            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
675
676        Ok(())
677    }
678
679    /// Save polygons (copper pours) to a file path.
680    ///
681    /// This performs a read-modify-write operation.
682    pub fn save_polygons_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
683        let file = std::fs::OpenOptions::new()
684            .read(true)
685            .write(true)
686            .open(path.as_ref())?;
687
688        let mut cf = CompoundFile::open(file).map_err(|e| {
689            AltiumError::Io(std::io::Error::new(
690                std::io::ErrorKind::InvalidData,
691                e.to_string(),
692            ))
693        })?;
694
695        let data_path = "/Polygons6/Data";
696
697        // Check if stream exists
698        if cf.entry(data_path).is_err() {
699            return Err(AltiumError::Parse(
700                "Polygons6/Data stream not found".to_string(),
701            ));
702        }
703
704        // Serialize all polygons to a buffer
705        let mut buffer = Vec::new();
706        for prim in &self.primitives {
707            if let PcbRecord::Polygon(p) = prim {
708                let params = p.to_params();
709                write_parameters_block(&mut buffer, &params)?;
710            }
711        }
712
713        // Open and write the stream
714        let mut stream = cf.open_stream(data_path).map_err(|e| {
715            AltiumError::Io(std::io::Error::new(
716                std::io::ErrorKind::NotFound,
717                e.to_string(),
718            ))
719        })?;
720
721        stream.seek(SeekFrom::Start(0))?;
722        stream.write_all(&buffer)?;
723
724        let new_len = buffer.len() as u64;
725        stream
726            .set_len(new_len)
727            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
728
729        cf.flush()
730            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
731
732        Ok(())
733    }
734
735    /// Get the number of components.
736    pub fn component_count(&self) -> usize {
737        self.components.len()
738    }
739
740    /// Get the number of primitives.
741    pub fn primitive_count(&self) -> usize {
742        self.primitives.len()
743    }
744
745    /// Get the number of nets.
746    pub fn net_count(&self) -> usize {
747        self.nets.len()
748    }
749
750    /// Get the number of design rules.
751    pub fn rule_count(&self) -> usize {
752        self.rules.len()
753    }
754
755    /// Iterate over design rules.
756    pub fn iter_rules(&self) -> impl Iterator<Item = &PcbRule> {
757        self.rules.iter()
758    }
759
760    /// Iterate over design rules mutably.
761    pub fn iter_rules_mut(&mut self) -> impl Iterator<Item = &mut PcbRule> {
762        self.rules.iter_mut()
763    }
764
765    /// Add a design rule.
766    pub fn add_rule(&mut self, rule: PcbRule) {
767        self.rules.push(rule);
768    }
769
770    /// Find a rule by name.
771    pub fn find_rule(&self, name: &str) -> Option<&PcbRule> {
772        self.rules.iter().find(|r| r.name == name)
773    }
774
775    /// Find a rule by name mutably.
776    pub fn find_rule_mut(&mut self, name: &str) -> Option<&mut PcbRule> {
777        self.rules.iter_mut().find(|r| r.name == name)
778    }
779
780    /// Iterate over components.
781    pub fn iter_components(&self) -> impl Iterator<Item = &PcbDocComponent> {
782        self.components.iter()
783    }
784
785    /// Iterate over primitives.
786    pub fn iter_primitives(&self) -> impl Iterator<Item = &PcbRecord> {
787        self.primitives.iter()
788    }
789
790    /// Count tracks.
791    pub fn track_count(&self) -> usize {
792        self.primitives
793            .iter()
794            .filter(|p| matches!(p, PcbRecord::Track(_)))
795            .count()
796    }
797
798    /// Count vias.
799    pub fn via_count(&self) -> usize {
800        self.primitives
801            .iter()
802            .filter(|p| matches!(p, PcbRecord::Via(_)))
803            .count()
804    }
805
806    /// Count pads (from components).
807    pub fn pad_count(&self) -> usize {
808        self.components
809            .iter()
810            .flat_map(|c| &c.primitives)
811            .filter(|p| matches!(p, PcbRecord::Pad(_)))
812            .count()
813    }
814
815    /// Find a component by designator.
816    pub fn find_component(&self, designator: &str) -> Option<&PcbDocComponent> {
817        self.components
818            .iter()
819            .find(|c| c.designator.eq_ignore_ascii_case(designator))
820    }
821
822    /// Find a component by designator mutably.
823    pub fn find_component_mut(&mut self, designator: &str) -> Option<&mut PcbDocComponent> {
824        self.components
825            .iter_mut()
826            .find(|c| c.designator.eq_ignore_ascii_case(designator))
827    }
828
829    /// Iterate over components mutably.
830    pub fn iter_components_mut(&mut self) -> impl Iterator<Item = &mut PcbDocComponent> {
831        self.components.iter_mut()
832    }
833
834    /// Write components to the CFB file.
835    fn write_components<R: Read + Write + Seek>(&self, cf: &mut CompoundFile<R>) -> Result<()> {
836        use crate::io::writer::write_parameters_block;
837
838        let data_path = "/Components6/Data";
839
840        // Check if stream exists
841        if cf.entry(data_path).is_err() {
842            return Err(AltiumError::Parse(
843                "Components6/Data stream not found".to_string(),
844            ));
845        }
846
847        // Serialize all components to a buffer
848        let mut buffer = Vec::new();
849        for component in &self.components {
850            write_parameters_block(&mut buffer, &component.params)?;
851        }
852
853        // Open and truncate the stream
854        let mut stream = cf.open_stream(data_path).map_err(|e| {
855            AltiumError::Io(std::io::Error::new(
856                std::io::ErrorKind::NotFound,
857                e.to_string(),
858            ))
859        })?;
860
861        // Seek to beginning and write
862        stream.seek(SeekFrom::Start(0))?;
863        stream.write_all(&buffer)?;
864
865        // Truncate to new length
866        let new_len = buffer.len() as u64;
867        stream
868            .set_len(new_len)
869            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
870
871        Ok(())
872    }
873
874    /// Save with component changes.
875    pub fn save_with_components<P: AsRef<Path>>(&self, path: P) -> Result<()> {
876        // Read the existing file
877        let file = std::fs::OpenOptions::new()
878            .read(true)
879            .write(true)
880            .open(path.as_ref())?;
881
882        let mut cf = CompoundFile::open(file).map_err(|e| {
883            AltiumError::Io(std::io::Error::new(
884                std::io::ErrorKind::InvalidData,
885                e.to_string(),
886            ))
887        })?;
888
889        // Write rules
890        self.write_rules(&mut cf)?;
891
892        // Write components
893        self.write_components(&mut cf)?;
894
895        cf.flush()
896            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
897
898        Ok(())
899    }
900
901    /// Save all primitives to a file path.
902    ///
903    /// This comprehensive save method writes all primitive types:
904    /// - Tracks
905    /// - Vias
906    /// - Arcs
907    /// - Fills
908    /// - Regions
909    /// - Polygons
910    /// - Components
911    /// - Rules
912    pub fn save_all_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
913        let file = std::fs::OpenOptions::new()
914            .read(true)
915            .write(true)
916            .open(path.as_ref())?;
917
918        let mut cf = CompoundFile::open(file).map_err(|e| {
919            AltiumError::Io(std::io::Error::new(
920                std::io::ErrorKind::InvalidData,
921                e.to_string(),
922            ))
923        })?;
924
925        // Write tracks
926        self.write_tracks(&mut cf)?;
927
928        // Write vias
929        self.write_vias(&mut cf)?;
930
931        // Write arcs
932        self.write_arcs(&mut cf)?;
933
934        // Write fills
935        self.write_fills(&mut cf)?;
936
937        // Write regions
938        self.write_regions_internal(&mut cf)?;
939
940        // Write polygons
941        self.write_polygons_internal(&mut cf)?;
942
943        // Write pads
944        self.write_pads(&mut cf)?;
945
946        // Write texts
947        self.write_texts(&mut cf)?;
948
949        // Write rules
950        self.write_rules(&mut cf)?;
951
952        // Write components
953        self.write_components(&mut cf)?;
954
955        cf.flush()
956            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
957
958        Ok(())
959    }
960
961    /// Write tracks to the CFB file.
962    fn write_tracks<R: Read + Write + Seek>(&self, cf: &mut CompoundFile<R>) -> Result<()> {
963        use crate::io::writer::write_block;
964        use crate::traits::ToBinary;
965        use byteorder::WriteBytesExt;
966
967        let data_path = "/Tracks6/Data";
968
969        if cf.entry(data_path).is_err() {
970            return Ok(()); // Stream doesn't exist, skip
971        }
972
973        let mut buffer = Vec::new();
974        for prim in &self.primitives {
975            if let PcbRecord::Track(track) = prim {
976                // Write RecordID byte
977                buffer.write_u8(PcbObjectId::Track.to_byte())?;
978                // Write size and data
979                let mut track_data = Vec::new();
980                track.write_to(&mut track_data)?;
981                write_block(&mut buffer, &track_data, 0)?;
982            }
983        }
984
985        let mut stream = cf.open_stream(data_path).map_err(|e| {
986            AltiumError::Io(std::io::Error::new(
987                std::io::ErrorKind::NotFound,
988                e.to_string(),
989            ))
990        })?;
991
992        stream.seek(SeekFrom::Start(0))?;
993        stream.write_all(&buffer)?;
994        stream
995            .set_len(buffer.len() as u64)
996            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
997
998        Ok(())
999    }
1000
1001    /// Write vias to the CFB file.
1002    fn write_vias<R: Read + Write + Seek>(&self, cf: &mut CompoundFile<R>) -> Result<()> {
1003        use crate::io::writer::write_block;
1004        use crate::traits::ToBinary;
1005        use byteorder::WriteBytesExt;
1006
1007        let data_path = "/Vias6/Data";
1008
1009        if cf.entry(data_path).is_err() {
1010            return Ok(());
1011        }
1012
1013        let mut buffer = Vec::new();
1014        for prim in &self.primitives {
1015            if let PcbRecord::Via(via) = prim {
1016                // Write RecordID byte
1017                buffer.write_u8(PcbObjectId::Via.to_byte())?;
1018                // Write size and data
1019                let mut via_data = Vec::new();
1020                via.write_to(&mut via_data)?;
1021                write_block(&mut buffer, &via_data, 0)?;
1022            }
1023        }
1024
1025        let mut stream = cf.open_stream(data_path).map_err(|e| {
1026            AltiumError::Io(std::io::Error::new(
1027                std::io::ErrorKind::NotFound,
1028                e.to_string(),
1029            ))
1030        })?;
1031
1032        stream.seek(SeekFrom::Start(0))?;
1033        stream.write_all(&buffer)?;
1034        stream
1035            .set_len(buffer.len() as u64)
1036            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
1037
1038        Ok(())
1039    }
1040
1041    /// Write arcs to the CFB file.
1042    fn write_arcs<R: Read + Write + Seek>(&self, cf: &mut CompoundFile<R>) -> Result<()> {
1043        use crate::io::writer::write_block;
1044        use crate::traits::ToBinary;
1045        use byteorder::WriteBytesExt;
1046
1047        let data_path = "/Arcs6/Data";
1048
1049        if cf.entry(data_path).is_err() {
1050            return Ok(());
1051        }
1052
1053        let mut buffer = Vec::new();
1054        for prim in &self.primitives {
1055            if let PcbRecord::Arc(arc) = prim {
1056                // Write RecordID byte
1057                buffer.write_u8(PcbObjectId::Arc.to_byte())?;
1058                // Write size and data
1059                let mut arc_data = Vec::new();
1060                arc.write_to(&mut arc_data)?;
1061                write_block(&mut buffer, &arc_data, 0)?;
1062            }
1063        }
1064
1065        let mut stream = cf.open_stream(data_path).map_err(|e| {
1066            AltiumError::Io(std::io::Error::new(
1067                std::io::ErrorKind::NotFound,
1068                e.to_string(),
1069            ))
1070        })?;
1071
1072        stream.seek(SeekFrom::Start(0))?;
1073        stream.write_all(&buffer)?;
1074        stream
1075            .set_len(buffer.len() as u64)
1076            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
1077
1078        Ok(())
1079    }
1080
1081    /// Write fills to the CFB file.
1082    fn write_fills<R: Read + Write + Seek>(&self, cf: &mut CompoundFile<R>) -> Result<()> {
1083        use crate::io::writer::write_block;
1084        use crate::traits::ToBinary;
1085        use byteorder::WriteBytesExt;
1086
1087        let data_path = "/Fills6/Data";
1088
1089        if cf.entry(data_path).is_err() {
1090            return Ok(());
1091        }
1092
1093        let mut buffer = Vec::new();
1094        for prim in &self.primitives {
1095            if let PcbRecord::Fill(fill) = prim {
1096                // Write RecordID byte
1097                buffer.write_u8(PcbObjectId::Fill.to_byte())?;
1098                // Write size and data
1099                let mut fill_data = Vec::new();
1100                fill.write_to(&mut fill_data)?;
1101                write_block(&mut buffer, &fill_data, 0)?;
1102            }
1103        }
1104
1105        let mut stream = cf.open_stream(data_path).map_err(|e| {
1106            AltiumError::Io(std::io::Error::new(
1107                std::io::ErrorKind::NotFound,
1108                e.to_string(),
1109            ))
1110        })?;
1111
1112        stream.seek(SeekFrom::Start(0))?;
1113        stream.write_all(&buffer)?;
1114        stream
1115            .set_len(buffer.len() as u64)
1116            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
1117
1118        Ok(())
1119    }
1120
1121    /// Write pads to the CFB file.
1122    fn write_pads<R: Read + Write + Seek>(&self, cf: &mut CompoundFile<R>) -> Result<()> {
1123        use crate::io::writer::write_block;
1124        use crate::traits::ToBinary;
1125        use byteorder::WriteBytesExt;
1126
1127        let data_path = "/Pads6/Data";
1128
1129        if cf.entry(data_path).is_err() {
1130            return Ok(());
1131        }
1132
1133        let mut buffer = Vec::new();
1134        for prim in &self.primitives {
1135            if let PcbRecord::Pad(pad) = prim {
1136                // Write RecordID byte
1137                buffer.write_u8(PcbObjectId::Pad.to_byte())?;
1138                // Write size and data
1139                let mut pad_data = Vec::new();
1140                pad.write_to(&mut pad_data)?;
1141                write_block(&mut buffer, &pad_data, 0)?;
1142            }
1143        }
1144
1145        let mut stream = cf.open_stream(data_path).map_err(|e| {
1146            AltiumError::Io(std::io::Error::new(
1147                std::io::ErrorKind::NotFound,
1148                e.to_string(),
1149            ))
1150        })?;
1151
1152        stream.seek(SeekFrom::Start(0))?;
1153        stream.write_all(&buffer)?;
1154        stream
1155            .set_len(buffer.len() as u64)
1156            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
1157
1158        Ok(())
1159    }
1160
1161    /// Write texts to the CFB file.
1162    fn write_texts<R: Read + Write + Seek>(&self, cf: &mut CompoundFile<R>) -> Result<()> {
1163        use crate::io::writer::write_block;
1164        use crate::traits::ToBinary;
1165        use byteorder::WriteBytesExt;
1166
1167        let data_path = "/Texts6/Data";
1168
1169        if cf.entry(data_path).is_err() {
1170            return Ok(());
1171        }
1172
1173        let mut buffer = Vec::new();
1174        for prim in &self.primitives {
1175            if let PcbRecord::Text(text) = prim {
1176                // Write RecordID byte
1177                buffer.write_u8(PcbObjectId::Text.to_byte())?;
1178                // Write size and data
1179                let mut text_data = Vec::new();
1180                text.write_to(&mut text_data)?;
1181                write_block(&mut buffer, &text_data, 0)?;
1182            }
1183        }
1184
1185        let mut stream = cf.open_stream(data_path).map_err(|e| {
1186            AltiumError::Io(std::io::Error::new(
1187                std::io::ErrorKind::NotFound,
1188                e.to_string(),
1189            ))
1190        })?;
1191
1192        stream.seek(SeekFrom::Start(0))?;
1193        stream.write_all(&buffer)?;
1194        stream
1195            .set_len(buffer.len() as u64)
1196            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
1197
1198        Ok(())
1199    }
1200
1201    /// Write regions to the CFB file (internal method).
1202    fn write_regions_internal<R: Read + Write + Seek>(
1203        &self,
1204        cf: &mut CompoundFile<R>,
1205    ) -> Result<()> {
1206        use crate::io::writer::write_block;
1207        use crate::traits::ToBinary;
1208        use byteorder::WriteBytesExt;
1209
1210        let data_path = "/Regions6/Data";
1211
1212        if cf.entry(data_path).is_err() {
1213            return Ok(());
1214        }
1215
1216        let mut buffer = Vec::new();
1217        for prim in &self.primitives {
1218            if let PcbRecord::Region(region) = prim {
1219                // Write RecordID byte
1220                buffer.write_u8(PcbObjectId::Region.to_byte())?;
1221                // Write size and data
1222                let mut region_data = Vec::new();
1223                region.write_to(&mut region_data)?;
1224                write_block(&mut buffer, &region_data, 0)?;
1225            }
1226        }
1227
1228        let mut stream = cf.open_stream(data_path).map_err(|e| {
1229            AltiumError::Io(std::io::Error::new(
1230                std::io::ErrorKind::NotFound,
1231                e.to_string(),
1232            ))
1233        })?;
1234
1235        stream.seek(SeekFrom::Start(0))?;
1236        stream.write_all(&buffer)?;
1237        stream
1238            .set_len(buffer.len() as u64)
1239            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
1240
1241        Ok(())
1242    }
1243
1244    /// Write polygons to the CFB file (internal method).
1245    fn write_polygons_internal<R: Read + Write + Seek>(
1246        &self,
1247        cf: &mut CompoundFile<R>,
1248    ) -> Result<()> {
1249        let data_path = "/Polygons6/Data";
1250
1251        if cf.entry(data_path).is_err() {
1252            return Ok(());
1253        }
1254
1255        let mut buffer = Vec::new();
1256        for prim in &self.primitives {
1257            if let PcbRecord::Polygon(polygon) = prim {
1258                let params = polygon.to_params();
1259                write_parameters_block(&mut buffer, &params)?;
1260            }
1261        }
1262
1263        let mut stream = cf.open_stream(data_path).map_err(|e| {
1264            AltiumError::Io(std::io::Error::new(
1265                std::io::ErrorKind::NotFound,
1266                e.to_string(),
1267            ))
1268        })?;
1269
1270        stream.seek(SeekFrom::Start(0))?;
1271        stream.write_all(&buffer)?;
1272        stream
1273            .set_len(buffer.len() as u64)
1274            .map_err(|e| AltiumError::Io(std::io::Error::other(e.to_string())))?;
1275
1276        Ok(())
1277    }
1278
1279    /// Count arcs.
1280    pub fn arc_count(&self) -> usize {
1281        self.primitives
1282            .iter()
1283            .filter(|p| matches!(p, PcbRecord::Arc(_)))
1284            .count()
1285    }
1286
1287    /// Count fills.
1288    pub fn fill_count(&self) -> usize {
1289        self.primitives
1290            .iter()
1291            .filter(|p| matches!(p, PcbRecord::Fill(_)))
1292            .count()
1293    }
1294
1295    /// Count regions.
1296    pub fn region_count(&self) -> usize {
1297        self.primitives
1298            .iter()
1299            .filter(|p| matches!(p, PcbRecord::Region(_)))
1300            .count()
1301    }
1302
1303    /// Count polygons.
1304    pub fn polygon_count(&self) -> usize {
1305        self.primitives
1306            .iter()
1307            .filter(|p| matches!(p, PcbRecord::Polygon(_)))
1308            .count()
1309    }
1310
1311    /// Count text elements.
1312    pub fn text_count(&self) -> usize {
1313        self.primitives
1314            .iter()
1315            .filter(|p| matches!(p, PcbRecord::Text(_)))
1316            .count()
1317    }
1318
1319    /// Add a track.
1320    pub fn add_track(&mut self, track: PcbTrack) {
1321        self.primitives.push(PcbRecord::Track(track));
1322    }
1323
1324    /// Add a via.
1325    pub fn add_via(&mut self, via: PcbVia) {
1326        self.primitives.push(PcbRecord::Via(via));
1327    }
1328
1329    /// Add an arc.
1330    pub fn add_arc(&mut self, arc: PcbArc) {
1331        self.primitives.push(PcbRecord::Arc(arc));
1332    }
1333
1334    /// Add a fill.
1335    pub fn add_fill(&mut self, fill: PcbFill) {
1336        self.primitives.push(PcbRecord::Fill(fill));
1337    }
1338
1339    /// Add a region.
1340    pub fn add_region(&mut self, region: PcbRegion) {
1341        self.primitives.push(PcbRecord::Region(region));
1342    }
1343
1344    /// Add a polygon.
1345    pub fn add_polygon(&mut self, polygon: PcbPolygon) {
1346        self.primitives.push(PcbRecord::Polygon(polygon));
1347    }
1348
1349    /// Remove primitive at index.
1350    pub fn remove_primitive(&mut self, index: usize) -> Option<PcbRecord> {
1351        if index < self.primitives.len() {
1352            Some(self.primitives.remove(index))
1353        } else {
1354            None
1355        }
1356    }
1357
1358    /// Get primitive at index.
1359    pub fn get_primitive(&self, index: usize) -> Option<&PcbRecord> {
1360        self.primitives.get(index)
1361    }
1362
1363    /// Get mutable primitive at index.
1364    pub fn get_primitive_mut(&mut self, index: usize) -> Option<&mut PcbRecord> {
1365        self.primitives.get_mut(index)
1366    }
1367
1368    /// Iterate over tracks.
1369    pub fn iter_tracks(&self) -> impl Iterator<Item = &PcbTrack> {
1370        self.primitives.iter().filter_map(|p| {
1371            if let PcbRecord::Track(t) = p {
1372                Some(t)
1373            } else {
1374                None
1375            }
1376        })
1377    }
1378
1379    /// Iterate over vias.
1380    pub fn iter_vias(&self) -> impl Iterator<Item = &PcbVia> {
1381        self.primitives.iter().filter_map(|p| {
1382            if let PcbRecord::Via(v) = p {
1383                Some(v)
1384            } else {
1385                None
1386            }
1387        })
1388    }
1389
1390    /// Iterate over arcs.
1391    pub fn iter_arcs(&self) -> impl Iterator<Item = &PcbArc> {
1392        self.primitives.iter().filter_map(|p| {
1393            if let PcbRecord::Arc(a) = p {
1394                Some(a)
1395            } else {
1396                None
1397            }
1398        })
1399    }
1400
1401    /// Iterate over fills.
1402    pub fn iter_fills(&self) -> impl Iterator<Item = &PcbFill> {
1403        self.primitives.iter().filter_map(|p| {
1404            if let PcbRecord::Fill(f) = p {
1405                Some(f)
1406            } else {
1407                None
1408            }
1409        })
1410    }
1411
1412    /// Iterate over regions.
1413    pub fn iter_regions(&self) -> impl Iterator<Item = &PcbRegion> {
1414        self.primitives.iter().filter_map(|p| {
1415            if let PcbRecord::Region(r) = p {
1416                Some(r)
1417            } else {
1418                None
1419            }
1420        })
1421    }
1422
1423    /// Iterate over polygons.
1424    pub fn iter_polygons(&self) -> impl Iterator<Item = &PcbPolygon> {
1425        self.primitives.iter().filter_map(|p| {
1426            if let PcbRecord::Polygon(pol) = p {
1427                Some(pol)
1428            } else {
1429                None
1430            }
1431        })
1432    }
1433
1434    /// Iterate over texts.
1435    pub fn iter_texts(&self) -> impl Iterator<Item = &PcbText> {
1436        self.primitives.iter().filter_map(|p| {
1437            if let PcbRecord::Text(t) = p {
1438                Some(t)
1439            } else {
1440                None
1441            }
1442        })
1443    }
1444
1445    /// Add a text annotation.
1446    pub fn add_text(&mut self, text: PcbText) {
1447        self.primitives.push(PcbRecord::Text(text));
1448    }
1449}
1450
1451impl PcbDocComponent {
1452    /// Get the X position of the component.
1453    pub fn x(&self) -> Option<crate::types::Coord> {
1454        self.params
1455            .get("X")
1456            .map(|v| v.as_coord_or(crate::types::Coord::ZERO))
1457    }
1458
1459    /// Get the Y position of the component.
1460    pub fn y(&self) -> Option<crate::types::Coord> {
1461        self.params
1462            .get("Y")
1463            .map(|v| v.as_coord_or(crate::types::Coord::ZERO))
1464    }
1465
1466    /// Get the rotation angle in degrees.
1467    pub fn rotation(&self) -> f64 {
1468        self.params
1469            .get("ROTATION")
1470            .and_then(|v| v.as_str().trim().parse::<f64>().ok())
1471            .unwrap_or(0.0)
1472    }
1473
1474    /// Get the layer.
1475    pub fn layer(&self) -> crate::types::Layer {
1476        self.params
1477            .get("LAYER")
1478            .and_then(|v| {
1479                let layer_str = v.as_str();
1480                // Try exact match first
1481                crate::types::Layer::from_name(layer_str).or_else(|| {
1482                    // Try common aliases
1483                    match layer_str.to_uppercase().as_str() {
1484                        "TOP" => Some(crate::types::Layer::TOP_LAYER),
1485                        "BOTTOM" => Some(crate::types::Layer::BOTTOM_LAYER),
1486                        "TOPOVERLAY" | "TOP_OVERLAY" => Some(crate::types::Layer::TOP_OVERLAY),
1487                        "BOTTOMOVERLAY" | "BOTTOM_OVERLAY" => {
1488                            Some(crate::types::Layer::BOTTOM_OVERLAY)
1489                        }
1490                        _ => None,
1491                    }
1492                })
1493            })
1494            .unwrap_or(crate::types::Layer::TOP_LAYER)
1495    }
1496
1497    /// Set the X position of the component.
1498    pub fn set_x(&mut self, x: crate::types::Coord) {
1499        self.params.add_coord("X", x);
1500    }
1501
1502    /// Set the Y position of the component.
1503    pub fn set_y(&mut self, y: crate::types::Coord) {
1504        self.params.add_coord("Y", y);
1505    }
1506
1507    /// Set the position of the component.
1508    pub fn set_position(&mut self, x: crate::types::Coord, y: crate::types::Coord) {
1509        self.set_x(x);
1510        self.set_y(y);
1511    }
1512
1513    /// Set the rotation angle in degrees.
1514    pub fn set_rotation(&mut self, rotation: f64) {
1515        // Format as scientific notation like Altium does
1516        self.params.add("ROTATION", &format!("{:.14E}", rotation));
1517    }
1518
1519    /// Set the layer.
1520    pub fn set_layer(&mut self, layer: crate::types::Layer) {
1521        self.params.add("LAYER", layer.name());
1522    }
1523}
1524
1525impl DumpTree for PcbDoc {
1526    fn dump(&self, tree: &mut TreeBuilder) {
1527        tree.root(&format!(
1528            "PcbDoc ({} components, {} primitives, {} rules)",
1529            self.components.len(),
1530            self.primitives.len(),
1531            self.rules.len()
1532        ));
1533
1534        // Summary
1535        tree.push(
1536            !self.components.is_empty() || !self.primitives.is_empty() || !self.rules.is_empty(),
1537        );
1538        tree.add_leaf(
1539            "Summary",
1540            &[
1541                ("components", format!("{}", self.components.len())),
1542                ("tracks", format!("{}", self.track_count())),
1543                ("vias", format!("{}", self.via_count())),
1544                ("nets", format!("{}", self.nets.len())),
1545                ("rules", format!("{}", self.rules.len())),
1546                ("primitives", format!("{}", self.primitives.len())),
1547            ],
1548        );
1549        tree.pop();
1550
1551        // Components
1552        if !self.components.is_empty() {
1553            tree.push(!self.primitives.is_empty());
1554            tree.begin_node(&format!("Components ({})", self.components.len()));
1555            for (i, comp) in self.components.iter().enumerate() {
1556                tree.push(i < self.components.len() - 1);
1557                comp.dump(tree);
1558                tree.pop();
1559            }
1560            tree.pop();
1561        }
1562
1563        // Nets
1564        if !self.nets.is_empty() {
1565            tree.push(false);
1566            tree.add_leaf(
1567                &format!("Nets ({})", self.nets.len()),
1568                &[(
1569                    "first_few",
1570                    self.nets
1571                        .iter()
1572                        .take(5)
1573                        .cloned()
1574                        .collect::<Vec<_>>()
1575                        .join(", "),
1576                )],
1577            );
1578            tree.pop();
1579        }
1580    }
1581}
1582
1583impl DumpTree for PcbDocComponent {
1584    fn dump(&self, tree: &mut TreeBuilder) {
1585        let mut props = vec![("designator", self.designator.clone())];
1586        if !self.pattern.is_empty() {
1587            props.push(("pattern", self.pattern.clone()));
1588        }
1589        if !self.comment.is_empty() {
1590            props.push(("comment", self.comment.clone()));
1591        }
1592        tree.add_leaf_with_params("Component", &props, Some(&self.params));
1593    }
1594}
1595
1596#[cfg(test)]
1597mod tests {
1598    use super::*;
1599    use std::io::Cursor;
1600
1601    #[test]
1602    fn test_read_classes_and_options() {
1603        let data = std::fs::read("data/PCB1.PcbDoc").expect("Failed to read file");
1604        let pcbdoc = PcbDoc::open(Cursor::new(&data)).expect("Failed to parse PcbDoc");
1605
1606        // Should have classes
1607        assert!(!pcbdoc.classes.is_empty(), "Should have parsed classes");
1608        println!("Classes: {}", pcbdoc.classes.len());
1609        for class in &pcbdoc.classes {
1610            println!("  - {} ({:?})", class.name, class.kind);
1611        }
1612
1613        // Should have placer options
1614        assert!(
1615            pcbdoc.placer_options.is_some(),
1616            "Should have placer options"
1617        );
1618        let opts = pcbdoc.placer_options.as_ref().unwrap();
1619        assert!(opts.use_rotation); // Default is true
1620
1621        // Should have DRC options
1622        assert!(pcbdoc.drc_options.is_some(), "Should have DRC options");
1623
1624        // Should have pin swap options
1625        assert!(
1626            pcbdoc.pin_swap_options.is_some(),
1627            "Should have pin swap options"
1628        );
1629
1630        // Should have rules
1631        assert!(!pcbdoc.rules.is_empty(), "Should have rules");
1632        println!("Rules: {}", pcbdoc.rules.len());
1633    }
1634}