1use std::collections::HashMap;
11use std::fs::File;
12use std::io::{BufRead, BufReader, Read, Write};
13use std::path::{Path, PathBuf};
14
15use crate::dump::{DumpTree, TreeBuilder};
16use crate::error::{AltiumError, Result};
17
18#[derive(Debug, Clone, Default)]
20pub struct ProjectDocument {
21 pub path: String,
23 pub doc_type: DocumentType,
25 pub annotation_enabled: bool,
27 pub annotation_start_value: i32,
29 pub do_library_update: bool,
31 pub do_database_update: bool,
33 pub params: HashMap<String, String>,
35}
36
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
39pub enum DocumentType {
40 Schematic,
42 Pcb,
44 SchLib,
46 PcbLib,
48 IntLib,
50 OutputJob,
52 #[default]
54 Other,
55}
56
57impl DocumentType {
58 pub fn from_path(path: &str) -> Self {
60 let lower = path.to_lowercase();
61 if lower.ends_with(".schdoc") {
62 DocumentType::Schematic
63 } else if lower.ends_with(".pcbdoc") {
64 DocumentType::Pcb
65 } else if lower.ends_with(".schlib") {
66 DocumentType::SchLib
67 } else if lower.ends_with(".pcblib") {
68 DocumentType::PcbLib
69 } else if lower.ends_with(".intlib") {
70 DocumentType::IntLib
71 } else if lower.ends_with(".outjob") {
72 DocumentType::OutputJob
73 } else {
74 DocumentType::Other
75 }
76 }
77
78 pub fn display_name(&self) -> &'static str {
80 match self {
81 DocumentType::Schematic => "Schematic",
82 DocumentType::Pcb => "PCB",
83 DocumentType::SchLib => "Schematic Library",
84 DocumentType::PcbLib => "PCB Library",
85 DocumentType::IntLib => "Integrated Library",
86 DocumentType::OutputJob => "Output Job",
87 DocumentType::Other => "Other",
88 }
89 }
90}
91
92impl std::fmt::Display for DocumentType {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 write!(f, "{}", self.display_name())
95 }
96}
97
98#[derive(Debug, Clone, Default)]
100pub struct ProjectParameter {
101 pub name: String,
103 pub value: String,
105}
106
107#[derive(Debug, Clone, Default)]
109pub struct ErcMatrix {
110 pub rows: Vec<String>,
112}
113
114impl ErcMatrix {
115 pub fn get_level(&self, row: usize, col: usize) -> Option<char> {
117 if row < self.rows.len() {
118 self.rows[row].chars().nth(col)
119 } else {
120 None
121 }
122 }
123
124 pub fn decode_level(c: char) -> &'static str {
126 match c {
127 'N' => "No Report",
128 'W' => "Warning",
129 'E' => "Error",
130 'A' => "ActiveLow Warning",
131 'B' => "Bidirectional",
132 'O' => "Open",
133 'R' => "Report",
134 _ => "Unknown",
135 }
136 }
137}
138
139#[derive(Debug, Clone, Default)]
141pub struct ProjectVariant {
142 pub name: String,
144 pub description: String,
146 pub parameter_overrides: HashMap<String, String>,
148}
149
150#[derive(Debug, Clone, Default)]
152pub struct OutputGroup {
153 pub name: String,
155 pub output_type: String,
157 pub settings: HashMap<String, String>,
159}
160
161#[derive(Debug, Clone, Default)]
163pub struct PrjPcb {
164 pub path: Option<PathBuf>,
166 pub version: String,
168 pub hierarchy_mode: i32,
170 pub output_path: String,
172 pub annotation_start_value: i32,
174 pub documents: Vec<ProjectDocument>,
176 pub parameters: HashMap<String, String>,
178 pub erc_matrix: ErcMatrix,
180 pub variants: Vec<ProjectVariant>,
182 pub output_groups: Vec<OutputGroup>,
184 pub sections: HashMap<String, HashMap<String, String>>,
186}
187
188impl PrjPcb {
189 pub fn new() -> Self {
191 PrjPcb {
192 version: "1.0".to_string(),
193 ..Default::default()
194 }
195 }
196
197 pub fn open<R: Read>(reader: R) -> Result<Self> {
199 let buf_reader = BufReader::new(reader);
200 let mut prj = PrjPcb::default();
201
202 let mut current_section = String::new();
203 let mut current_section_data: HashMap<String, String> = HashMap::new();
204
205 for line_result in buf_reader.lines() {
206 let line = line_result.map_err(AltiumError::Io)?;
207 let trimmed = line.trim();
208
209 if trimmed.is_empty() {
211 continue;
212 }
213
214 if trimmed.starts_with('[') && trimmed.ends_with(']') {
216 if !current_section.is_empty() {
218 prj.process_section(¤t_section, ¤t_section_data);
219 prj.sections
220 .insert(current_section.clone(), current_section_data.clone());
221 }
222
223 current_section = trimmed[1..trimmed.len() - 1].to_string();
225 current_section_data = HashMap::new();
226 } else if let Some(eq_pos) = trimmed.find('=') {
227 let key = trimmed[..eq_pos].to_string();
229 let value = trimmed[eq_pos + 1..].to_string();
230 current_section_data.insert(key, value);
231 }
232 }
233
234 if !current_section.is_empty() {
236 prj.process_section(¤t_section, ¤t_section_data);
237 prj.sections.insert(current_section, current_section_data);
238 }
239
240 Ok(prj)
241 }
242
243 pub fn open_file<P: AsRef<Path>>(path: P) -> Result<Self> {
245 let path_ref = path.as_ref();
246 let file = File::open(path_ref)?;
247 let mut prj = Self::open(file)?;
248 prj.path = Some(path_ref.to_path_buf());
249 Ok(prj)
250 }
251
252 fn process_section(&mut self, section: &str, data: &HashMap<String, String>) {
254 match section {
255 "Design" => self.process_design_section(data),
256 "Parameters" => {
257 self.parameters = data.clone();
258 }
259 "ERC Connection Matrix" => self.process_erc_section(data),
260 _ => {
261 if let Some(suffix) = section.strip_prefix("Document") {
263 if suffix.parse::<i32>().is_ok() {
264 self.process_document_section(data);
265 }
266 }
267 if let Some(suffix) = section.strip_prefix("OutputGroup") {
269 if suffix.parse::<i32>().is_ok() {
270 self.process_output_group_section(section, data);
271 }
272 }
273 }
274 }
275 }
276
277 fn process_design_section(&mut self, data: &HashMap<String, String>) {
279 if let Some(v) = data.get("Version") {
280 self.version = v.clone();
281 }
282 if let Some(v) = data.get("HierarchyMode") {
283 self.hierarchy_mode = v.parse().unwrap_or(0);
284 }
285 if let Some(v) = data.get("OutputPath") {
286 self.output_path = v.clone();
287 }
288 if let Some(v) = data.get("AnnotationStartValue") {
289 self.annotation_start_value = v.parse().unwrap_or(1);
290 }
291 }
292
293 fn process_document_section(&mut self, data: &HashMap<String, String>) {
295 let path = data.get("DocumentPath").cloned().unwrap_or_default();
296 if path.is_empty() {
297 return;
298 }
299
300 let doc = ProjectDocument {
301 doc_type: DocumentType::from_path(&path),
302 path,
303 annotation_enabled: data
304 .get("AnnotationEnabled")
305 .map(|v| v == "1")
306 .unwrap_or(true),
307 annotation_start_value: data
308 .get("AnnotateStartValue")
309 .and_then(|v| v.parse().ok())
310 .unwrap_or(1),
311 do_library_update: data
312 .get("DoLibraryUpdate")
313 .map(|v| v == "1")
314 .unwrap_or(true),
315 do_database_update: data
316 .get("DoDatabaseUpdate")
317 .map(|v| v == "1")
318 .unwrap_or(true),
319 params: data.clone(),
320 };
321
322 self.documents.push(doc);
323 }
324
325 fn process_erc_section(&mut self, data: &HashMap<String, String>) {
327 let mut rows = Vec::new();
328 for i in 1..=17 {
329 let key = format!("L{}", i);
330 if let Some(v) = data.get(&key) {
331 rows.push(v.clone());
332 }
333 }
334 self.erc_matrix.rows = rows;
335 }
336
337 fn process_output_group_section(&mut self, section: &str, data: &HashMap<String, String>) {
339 let group = OutputGroup {
340 name: data
341 .get("Name")
342 .cloned()
343 .unwrap_or_else(|| section.to_string()),
344 output_type: data.get("OutputType").cloned().unwrap_or_default(),
345 settings: data.clone(),
346 };
347 self.output_groups.push(group);
348 }
349
350 pub fn save<W: Write>(&self, mut writer: W) -> Result<()> {
352 writeln!(writer, "[Design]")?;
354 writeln!(writer, "Version={}", self.version)?;
355 writeln!(writer, "HierarchyMode={}", self.hierarchy_mode)?;
356 writeln!(writer, "ChannelRoomNamingStyle=0")?;
357 writeln!(writer, "OutputPath={}", self.output_path)?;
358 writeln!(writer, "LogFolderPath=")?;
359 writeln!(
360 writer,
361 "AnnotationStartValue={}",
362 self.annotation_start_value
363 )?;
364 writeln!(writer, "OpenOutputs=1")?;
365 writeln!(writer, "ArchiveProject=0")?;
366 writeln!(writer, "TimestampOutput=0")?;
367 writeln!(writer, "ManagedProjectGuid=")?;
368 writeln!(writer, "Variants=")?;
369 writeln!(writer)?;
370
371 for (i, doc) in self.documents.iter().enumerate() {
373 writeln!(writer, "[Document{}]", i + 1)?;
374 writeln!(writer, "DocumentPath={}", doc.path)?;
375 writeln!(
376 writer,
377 "AnnotationEnabled={}",
378 if doc.annotation_enabled { "1" } else { "0" }
379 )?;
380 writeln!(writer, "AnnotateStartValue={}", doc.annotation_start_value)?;
381 writeln!(writer, "AnnotationIndexControlEnabled=0")?;
382 writeln!(writer, "AnnotateSuffix=")?;
383 writeln!(writer, "AnnotateScope=0")?;
384 writeln!(writer, "AnnotateOrder=-1")?;
385 writeln!(
386 writer,
387 "DoLibraryUpdate={}",
388 if doc.do_library_update { "1" } else { "0" }
389 )?;
390 writeln!(
391 writer,
392 "DoDatabaseUpdate={}",
393 if doc.do_database_update { "1" } else { "0" }
394 )?;
395 writeln!(writer, "ClassGenCCAutoEnabled=1")?;
396 writeln!(writer, "ClassGenCCAutoRoomEnabled=1")?;
397 writeln!(writer, "ClassGenNCAutoScope=0")?;
398 writeln!(writer, "DItemRevisionGUID=")?;
399 writeln!(writer)?;
400 }
401
402 writeln!(writer, "[GeneratedDocuments]")?;
404 writeln!(writer)?;
405
406 writeln!(writer, "[ProjectVariantGroups]")?;
408 writeln!(writer)?;
409
410 writeln!(writer, "[ERC Connection Matrix]")?;
412 let default_erc = vec![
413 "NNNNNNNNNNNWNNNWW",
414 "NNWNNNNWNWNWNWNWN",
415 "NWEABOROBWBWRORNB",
416 "NNAABOROBWBWBORNB",
417 "NNBBNNBNNWNWBNNNN",
418 "NNOOOROOONNWOONOO",
419 "NNRBBNRNNWNWRBNRN",
420 "NWOOOOOOOWOWNOOOO",
421 "NNBBNNBNNWNWBNNNN",
422 "NWWWWNWWWNWWWWNWW",
423 "WBBBBOBBBBBWBBBBB",
424 "NWWWWNWWWNWWWWNWW",
425 "WWRRRORRRWRWRRNRR",
426 "NNBBNNBNNWNWBNNNN",
427 "NNOOOROOONNWOONOO",
428 "WWNRNNRNRWRWRNRRR",
429 "WNNNNNNNNNBNNNNRN",
430 ];
431 let rows = if self.erc_matrix.rows.is_empty() {
432 &default_erc
433 } else {
434 &self
435 .erc_matrix
436 .rows
437 .iter()
438 .map(|s| s.as_str())
439 .collect::<Vec<_>>()
440 };
441 for (i, row) in rows.iter().enumerate() {
442 writeln!(writer, "L{}={}", i + 1, row)?;
443 }
444 writeln!(writer)?;
445
446 writeln!(writer, "[ProjectOptions]")?;
448 writeln!(writer, "IncludeDesignatorInPinUniqueIDNumber=0")?;
449 writeln!(writer, "IncludePartNumberInPinUniqueIDNumber=0")?;
450 writeln!(writer, "EnableConstraintManager=0")?;
451 writeln!(writer, "ComponentNamingScheme=0")?;
452 writeln!(writer, "PadNamingScheme=0")?;
453 writeln!(writer, "ComponentAutoZoom=1")?;
454 writeln!(writer, "ShowSheetNumberInSheetSymbolPad=0")?;
455 writeln!(writer, "OpenSchematicInServerForce=0")?;
456 writeln!(writer, "LocalCompilerGUID=")?;
457 writeln!(writer)?;
458
459 writeln!(writer, "[Parameters]")?;
461 for (key, value) in &self.parameters {
462 writeln!(writer, "{}={}", key, value)?;
463 }
464 writeln!(writer)?;
465
466 writeln!(writer, "[Workspace]")?;
468 writeln!(writer)?;
469
470 writeln!(writer, "[Configuration Constraints]")?;
472 writeln!(writer)?;
473
474 Ok(())
475 }
476
477 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
479 let file = File::create(path)?;
480 self.save(file)
481 }
482
483 pub fn add_document(&mut self, path: impl Into<String>) -> &mut ProjectDocument {
489 let path_str = path.into();
490 let doc = ProjectDocument {
491 doc_type: DocumentType::from_path(&path_str),
492 path: path_str,
493 annotation_enabled: true,
494 annotation_start_value: 1,
495 do_library_update: true,
496 do_database_update: true,
497 params: HashMap::new(),
498 };
499 self.documents.push(doc);
500 self.documents.last_mut().unwrap()
501 }
502
503 pub fn remove_document(&mut self, path: &str) -> bool {
505 let original_len = self.documents.len();
506 self.documents.retain(|d| d.path != path);
507 self.documents.len() != original_len
508 }
509
510 pub fn get_document(&self, path: &str) -> Option<&ProjectDocument> {
512 self.documents.iter().find(|d| d.path == path)
513 }
514
515 pub fn get_document_mut(&mut self, path: &str) -> Option<&mut ProjectDocument> {
517 self.documents.iter_mut().find(|d| d.path == path)
518 }
519
520 pub fn schematics(&self) -> Vec<&ProjectDocument> {
522 self.documents
523 .iter()
524 .filter(|d| d.doc_type == DocumentType::Schematic)
525 .collect()
526 }
527
528 pub fn pcb_documents(&self) -> Vec<&ProjectDocument> {
530 self.documents
531 .iter()
532 .filter(|d| d.doc_type == DocumentType::Pcb)
533 .collect()
534 }
535
536 pub fn primary_pcb(&self) -> Option<&ProjectDocument> {
538 self.documents
539 .iter()
540 .find(|d| d.doc_type == DocumentType::Pcb)
541 }
542
543 pub fn name(&self) -> String {
545 if let Some(name) = self.parameters.get("Name") {
546 name.clone()
547 } else if let Some(ref path) = self.path {
548 path.file_stem()
549 .and_then(|s| s.to_str())
550 .unwrap_or("Unnamed")
551 .to_string()
552 } else {
553 "Unnamed".to_string()
554 }
555 }
556
557 pub fn set_name(&mut self, name: impl Into<String>) {
559 self.parameters.insert("Name".to_string(), name.into());
560 }
561
562 pub fn get_parameter(&self, key: &str) -> Option<&String> {
568 self.parameters.get(key)
569 }
570
571 pub fn set_parameter(&mut self, key: impl Into<String>, value: impl Into<String>) {
573 self.parameters.insert(key.into(), value.into());
574 }
575
576 pub fn remove_parameter(&mut self, key: &str) -> Option<String> {
578 self.parameters.remove(key)
579 }
580}
581
582impl DumpTree for PrjPcb {
583 fn dump(&self, tree: &mut TreeBuilder) {
584 tree.root(&format!(
585 "Project: {} ({} documents)",
586 self.name(),
587 self.documents.len()
588 ));
589
590 tree.push(!self.documents.is_empty());
592 let info_props = vec![
593 ("version", self.version.clone()),
594 (
595 "hierarchy",
596 if self.hierarchy_mode == 0 {
597 "Flat".to_string()
598 } else {
599 "Hierarchical".to_string()
600 },
601 ),
602 ("output_path", self.output_path.clone()),
603 ];
604 tree.add_leaf("Info", &info_props);
605 tree.pop();
606
607 if !self.documents.is_empty() {
609 tree.push(!self.parameters.is_empty());
610 tree.begin_node(&format!("Documents ({})", self.documents.len()));
611 for (i, doc) in self.documents.iter().enumerate() {
612 tree.push(i < self.documents.len() - 1);
613 let doc_props = vec![
614 ("type", doc.doc_type.display_name().to_string()),
615 (
616 "annotation",
617 if doc.annotation_enabled {
618 "enabled".to_string()
619 } else {
620 "disabled".to_string()
621 },
622 ),
623 ];
624 tree.add_leaf(&doc.path, &doc_props);
625 tree.pop();
626 }
627 tree.pop();
628 }
629
630 if !self.parameters.is_empty() {
632 tree.push(false);
633 tree.begin_node(&format!("Parameters ({})", self.parameters.len()));
634 let mut params: Vec<_> = self.parameters.iter().collect();
635 params.sort_by_key(|(k, _)| k.as_str());
636 for (i, (key, value)) in params.iter().enumerate() {
637 tree.push(i < params.len() - 1);
638 tree.add_leaf(key, &[("value", value.to_string())]);
639 tree.pop();
640 }
641 tree.pop();
642 }
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649
650 #[test]
651 fn test_document_type_from_path() {
652 assert_eq!(
653 DocumentType::from_path("Sheet1.SchDoc"),
654 DocumentType::Schematic
655 );
656 assert_eq!(DocumentType::from_path("PCB1.PcbDoc"), DocumentType::Pcb);
657 assert_eq!(
658 DocumentType::from_path("Library.SchLib"),
659 DocumentType::SchLib
660 );
661 assert_eq!(
662 DocumentType::from_path("Library.PcbLib"),
663 DocumentType::PcbLib
664 );
665 assert_eq!(DocumentType::from_path("test.txt"), DocumentType::Other);
666 }
667
668 #[test]
669 fn test_parse_project() {
670 let content = r#"[Design]
671Version=1.0
672HierarchyMode=0
673OutputPath=Project Outputs\
674AnnotationStartValue=1
675
676[Document1]
677DocumentPath=Sheet1.SchDoc
678AnnotationEnabled=1
679
680[Document2]
681DocumentPath=PCB1.PcbDoc
682AnnotationEnabled=1
683
684[Parameters]
685Name=TestProject
686"#;
687
688 let prj = PrjPcb::open(content.as_bytes()).unwrap();
689 assert_eq!(prj.version, "1.0");
690 assert_eq!(prj.hierarchy_mode, 0);
691 assert_eq!(prj.documents.len(), 2);
692 assert_eq!(prj.documents[0].path, "Sheet1.SchDoc");
693 assert_eq!(prj.documents[0].doc_type, DocumentType::Schematic);
694 assert_eq!(prj.documents[1].path, "PCB1.PcbDoc");
695 assert_eq!(prj.documents[1].doc_type, DocumentType::Pcb);
696 assert_eq!(prj.name(), "TestProject");
697 }
698}