use crate::error::{Result, TransJlcError};
use regex::Regex;
use std::collections::HashMap;
use std::path::Path;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum LayerType {
NpthThrough,
PthThrough,
PthThroughVia,
BottomSilkscreen,
BottomSoldermask,
BottomPasteMask,
BottomCopper,
TopSilkscreen,
TopSoldermask,
TopPasteMask,
TopCopper,
BoardOutline,
InnerLayer(u32),
ColorfulTopSilkscreen,
ColorfulBottomSilkscreen,
ColorfulBoardOutline,
ColorfulBoardOutlineMark,
Other,
}
impl LayerType {
pub fn to_jlc_filename(&self) -> String {
match self {
LayerType::NpthThrough => "Drill_NPTH_Through.DRL".to_string(),
LayerType::PthThrough => "Drill_PTH_Through.DRL".to_string(),
LayerType::PthThroughVia => "Drill_PTH_Through_Via.DRL".to_string(),
LayerType::BottomSilkscreen => "Gerber_BottomSilkscreenLayer.GBO".to_string(),
LayerType::BottomSoldermask => "Gerber_BottomSolderMaskLayer.GBS".to_string(),
LayerType::BottomPasteMask => "Gerber_BottomPasteMaskLayer.GBP".to_string(),
LayerType::BottomCopper => "Gerber_BottomLayer.GBL".to_string(),
LayerType::TopSilkscreen => "Gerber_TopSilkscreenLayer.GTO".to_string(),
LayerType::TopSoldermask => "Gerber_TopSolderMaskLayer.GTS".to_string(),
LayerType::TopPasteMask => "Gerber_TopPasteMaskLayer.GTP".to_string(),
LayerType::TopCopper => "Gerber_TopLayer.GTL".to_string(),
LayerType::BoardOutline => "Gerber_BoardOutlineLayer.GKO".to_string(),
LayerType::InnerLayer(num) => format!("Gerber_InnerLayer{}.G{}", num, num),
LayerType::ColorfulTopSilkscreen => {
"Fabrication_ColorfulTopSilkscreen.FCTS".to_string()
}
LayerType::ColorfulBottomSilkscreen => {
"Fabrication_ColorfulBottomSilkscreen.FCBS".to_string()
}
LayerType::ColorfulBoardOutline => {
"Fabrication_ColorfulBoardOutlineLayer.FCBO".to_string()
}
LayerType::ColorfulBoardOutlineMark => {
"Fabrication_ColorfulBoardOutlineMark.FCBM".to_string()
}
LayerType::Other => "Unknown".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct EdaPatterns {
pub name: String,
patterns: HashMap<LayerType, Vec<String>>,
}
impl EdaPatterns {
pub fn new(name: String) -> Self {
Self {
name,
patterns: HashMap::new(),
}
}
pub fn add_pattern(&mut self, layer_type: LayerType, pattern: String) {
self.patterns.entry(layer_type).or_default().push(pattern);
}
pub fn match_filename(&self, filename: &str) -> Option<LayerType> {
for drill_layer in [
LayerType::NpthThrough,
LayerType::PthThrough,
LayerType::PthThroughVia,
] {
if let Some(drill_patterns) = self.patterns.get(&drill_layer) {
for pattern in drill_patterns {
match Regex::new(pattern) {
Ok(regex) if regex.is_match(filename) => {
debug!(
"Matched '{}' to {:?} using pattern '{}'",
filename, drill_layer, pattern
);
return Some(drill_layer);
}
Ok(_) => {}
Err(_) => warn!("Invalid regex pattern: {}", pattern),
}
}
}
}
for (layer_type, patterns) in &self.patterns {
if matches!(
layer_type,
LayerType::NpthThrough | LayerType::PthThrough | LayerType::PthThroughVia
) {
continue;
}
for pattern in patterns {
if let Ok(regex) = Regex::new(pattern) {
if regex.is_match(filename) {
debug!(
"Matched '{}' to {:?} using pattern '{}'",
filename, layer_type, pattern
);
if matches!(layer_type, LayerType::InnerLayer(_)) {
return self.extract_inner_layer_number(filename, ®ex);
}
return Some(layer_type.clone());
}
} else {
warn!("Invalid regex pattern: {}", pattern);
}
}
}
debug!("No pattern matched for filename: {}", filename);
None
}
fn extract_inner_layer_number(&self, filename: &str, regex: &Regex) -> Option<LayerType> {
if let Some(caps) = regex.captures(filename) {
for i in 1..caps.len() {
if let Some(matched) = caps.get(i) {
if let Ok(num) = matched.as_str().parse::<u32>() {
return Some(LayerType::InnerLayer(num));
}
}
}
}
let number_regex = Regex::new(r"(\d+)").ok()?;
if let Some(caps) = number_regex.captures(filename) {
if let Some(matched) = caps.get(1) {
if let Ok(num) = matched.as_str().parse::<u32>() {
return Some(LayerType::InnerLayer(num));
}
}
}
None
}
pub fn can_handle_files(&self, filenames: &[String]) -> bool {
let mut matched_types = std::collections::HashSet::new();
for filename in filenames {
if let Some(layer_type) = self.match_filename(filename) {
matched_types.insert(std::mem::discriminant(&layer_type));
}
}
let min_layer_types = 3;
debug!(
"Pattern '{}' matched {} different layer types from {} files",
self.name,
matched_types.len(),
filenames.len()
);
matched_types.len() >= min_layer_types
}
}
pub struct PatternMatcher;
impl PatternMatcher {
pub fn create_kicad_patterns() -> EdaPatterns {
let mut patterns = EdaPatterns::new("KiCad".to_string());
patterns.add_pattern(LayerType::NpthThrough, r"(?i)-?NPTH\.drl$".to_string());
patterns.add_pattern(LayerType::NpthThrough, r"(?i)NPTH\.drl$".to_string());
patterns.add_pattern(LayerType::PthThrough, r"(?i)-?PTH\.drl$".to_string());
patterns.add_pattern(LayerType::PthThrough, r"(?i)PTH\.drl$".to_string());
patterns.add_pattern(LayerType::PthThrough, r"(?i)\.drl$".to_string());
patterns.add_pattern(LayerType::TopCopper, r"-F_Cu\.gbr$".to_string());
patterns.add_pattern(LayerType::BottomCopper, r"-B_Cu\.gbr$".to_string());
patterns.add_pattern(LayerType::InnerLayer(0), r"-In(\d+)_Cu\.gbr$".to_string());
patterns.add_pattern(LayerType::TopSoldermask, r"-F_Mask\.gbr$".to_string());
patterns.add_pattern(LayerType::BottomSoldermask, r"-B_Mask\.gbr$".to_string());
patterns.add_pattern(LayerType::TopPasteMask, r"-F_Paste\.gbr$".to_string());
patterns.add_pattern(LayerType::BottomPasteMask, r"-B_Paste\.gbr$".to_string());
patterns.add_pattern(LayerType::TopSilkscreen, r"-F_Silkscreen\.gbr$".to_string());
patterns.add_pattern(
LayerType::BottomSilkscreen,
r"-B_Silkscreen\.gbr$".to_string(),
);
patterns.add_pattern(LayerType::BoardOutline, r"-Edge_Cuts\.gbr$".to_string());
patterns
}
pub fn create_protel_patterns() -> EdaPatterns {
let mut patterns = EdaPatterns::new("Protel".to_string());
patterns.add_pattern(LayerType::TopCopper, r"(?i)\.gtl$".to_string());
patterns.add_pattern(LayerType::BottomCopper, r"(?i)\.gbl$".to_string());
patterns.add_pattern(LayerType::TopSoldermask, r"(?i)\.gts$".to_string());
patterns.add_pattern(LayerType::BottomSoldermask, r"(?i)\.gbs$".to_string());
patterns.add_pattern(LayerType::TopPasteMask, r"(?i)\.gtp$".to_string());
patterns.add_pattern(LayerType::BottomPasteMask, r"(?i)\.gbp$".to_string());
patterns.add_pattern(LayerType::TopSilkscreen, r"(?i)\.gto$".to_string());
patterns.add_pattern(LayerType::BottomSilkscreen, r"(?i)\.gbo$".to_string());
patterns.add_pattern(LayerType::BoardOutline, r"(?i)\.gko$".to_string());
patterns.add_pattern(LayerType::BoardOutline, r"(?i)\.gm1$".to_string()); patterns.add_pattern(LayerType::BoardOutline, r"(?i)\.outline$".to_string());
patterns.add_pattern(LayerType::BoardOutline, r"(?i)\.oln$".to_string());
patterns.add_pattern(LayerType::InnerLayer(0), r"(?i)\.g(\d+)$".to_string());
patterns.add_pattern(LayerType::InnerLayer(0), r"(?i)\.l(\d+)$".to_string());
patterns.add_pattern(LayerType::PthThrough, r"(?i)\.drl$".to_string());
patterns.add_pattern(LayerType::PthThrough, r"(?i)\.txt$".to_string()); patterns.add_pattern(LayerType::NpthThrough, r"(?i)npth\.drl$".to_string());
patterns.add_pattern(LayerType::NpthThrough, r"(?i)-npth\.drl$".to_string());
patterns
}
pub fn create_jlc_patterns() -> EdaPatterns {
let mut patterns = EdaPatterns::new("JLC".to_string());
patterns.add_pattern(
LayerType::NpthThrough,
r"^Drill_NPTH_Through\.DRL$".to_string(),
);
patterns.add_pattern(
LayerType::PthThrough,
r"^Drill_PTH_Through\.DRL$".to_string(),
);
patterns.add_pattern(
LayerType::PthThroughVia,
r"^Drill_PTH_Through_Via\.DRL$".to_string(),
);
patterns.add_pattern(
LayerType::BottomSilkscreen,
r"^Gerber_BottomSilkscreenLayer\.GBO$".to_string(),
);
patterns.add_pattern(
LayerType::BottomSoldermask,
r"^Gerber_BottomSolderMaskLayer\.GBS$".to_string(),
);
patterns.add_pattern(
LayerType::BottomPasteMask,
r"^Gerber_BottomPasteMaskLayer\.GBP$".to_string(),
);
patterns.add_pattern(
LayerType::BottomCopper,
r"^Gerber_BottomLayer\.GBL$".to_string(),
);
patterns.add_pattern(
LayerType::TopSilkscreen,
r"^Gerber_TopSilkscreenLayer\.GTO$".to_string(),
);
patterns.add_pattern(
LayerType::TopSoldermask,
r"^Gerber_TopSolderMaskLayer\.GTS$".to_string(),
);
patterns.add_pattern(
LayerType::TopPasteMask,
r"^Gerber_TopPasteMaskLayer\.GTP$".to_string(),
);
patterns.add_pattern(LayerType::TopCopper, r"^Gerber_TopLayer\.GTL$".to_string());
patterns.add_pattern(
LayerType::BoardOutline,
r"^Gerber_BoardOutlineLayer\.GKO$".to_string(),
);
patterns.add_pattern(
LayerType::InnerLayer(0),
r"^Gerber_InnerLayer(\d+)\.G(\d+)$".to_string(),
);
patterns
}
pub fn auto_detect_eda<P: AsRef<Path>>(files: &[P]) -> Result<EdaPatterns> {
let filenames: Vec<String> = files
.iter()
.filter_map(|p| {
p.as_ref()
.file_name()
.and_then(|name| name.to_str())
.map(|s| s.to_string())
})
.collect();
info!("Detecting EDA tool type for {} files...", filenames.len());
debug!("Files to analyze: {:?}", filenames);
for (i, filename) in filenames.iter().enumerate() {
debug!("File {}: {}", i + 1, filename);
}
let patterns_to_test = vec![
Self::create_kicad_patterns(),
Self::create_protel_patterns(),
Self::create_jlc_patterns(),
];
for pattern in patterns_to_test {
info!("Testing pattern matcher: {}", pattern.name);
let mut matches = 0;
for filename in &filenames {
if let Some(layer_type) = pattern.match_filename(filename) {
debug!(
"Pattern '{}' matched '{}' -> {:?}",
pattern.name, filename, layer_type
);
matches += 1;
}
}
debug!("Pattern '{}' matched {} files", pattern.name, matches);
if pattern.can_handle_files(&filenames) {
info!("Detected pattern: {}", &pattern.name);
return Ok(pattern);
} else {
debug!(
"Pattern '{}' cannot handle files (missing board outline)",
pattern.name
);
}
}
warn!("No known EDA pattern detected");
Err(TransJlcError::NoMatchingPattern.into())
}
pub fn create_custom_patterns(name: String) -> EdaPatterns {
warn!("Creating custom pattern matcher for: {}", name);
EdaPatterns::new(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kicad_pattern_matching() {
let patterns = PatternMatcher::create_kicad_patterns();
assert_eq!(
patterns.match_filename("project-Edge_Cuts.gbr"),
Some(LayerType::BoardOutline)
);
assert_eq!(
patterns.match_filename("project-F_Cu.gbr"),
Some(LayerType::TopCopper)
);
assert_eq!(
patterns.match_filename("project-B_Cu.gbr"),
Some(LayerType::BottomCopper)
);
assert_eq!(
patterns.match_filename("project-In1_Cu.gbr"),
Some(LayerType::InnerLayer(1))
);
}
#[test]
fn test_protel_pattern_matching() {
let patterns = PatternMatcher::create_protel_patterns();
assert_eq!(
patterns.match_filename("project.GTL"),
Some(LayerType::TopCopper)
);
assert_eq!(
patterns.match_filename("project.gtl"),
Some(LayerType::TopCopper)
);
assert_eq!(
patterns.match_filename("project.GKO"),
Some(LayerType::BoardOutline)
);
assert_eq!(
patterns.match_filename("project.TXT"),
Some(LayerType::PthThrough)
);
assert_eq!(patterns.match_filename("project.DRR"), None);
}
#[test]
fn test_layer_type_to_jlc_filename() {
assert_eq!(
LayerType::TopCopper.to_jlc_filename(),
"Gerber_TopLayer.GTL"
);
assert_eq!(
LayerType::InnerLayer(1).to_jlc_filename(),
"Gerber_InnerLayer1.G1"
);
assert_eq!(
LayerType::BoardOutline.to_jlc_filename(),
"Gerber_BoardOutlineLayer.GKO"
);
}
#[test]
fn test_can_handle_files() {
let patterns = PatternMatcher::create_kicad_patterns();
let files_with_multiple_layers = vec![
"project-F_Cu.gbr".to_string(),
"project-B_Cu.gbr".to_string(),
"project-F_Mask.gbr".to_string(),
"project-Edge_Cuts.gbr".to_string(),
];
let files_with_few_layers = vec!["project-F_Cu.gbr".to_string(), "unknown.txt".to_string()];
assert!(patterns.can_handle_files(&files_with_multiple_layers));
assert!(!patterns.can_handle_files(&files_with_few_layers));
}
}