package main import ( "bufio" "math" "os" "regexp" "strconv" "strings" ) // DrillHoleType classifies a drill hole by function type DrillHoleType int const ( DrillTypeUnknown DrillHoleType = iota DrillTypeVia // ViaDrill — ignore for enclosure DrillTypeComponent // ComponentDrill — component leads DrillTypeMounting // Mounting holes (from NPTH) ) // DrillHole represents a single drill hole with position, diameter, and type type DrillHole struct { X, Y float64 // Position in mm Diameter float64 // Diameter in mm Type DrillHoleType // Classified by TA.AperFunction ToolNum int // Tool number (T1, T2, etc.) } // ParseDrill parses an Excellon drill file and returns hole positions func ParseDrill(filename string) ([]DrillHole, error) { file, err := os.Open(filename) if err != nil { return nil, err } defer file.Close() var holes []DrillHole type toolInfo struct { diameter float64 holeType DrillHoleType } tools := make(map[int]toolInfo) currentTool := 0 inHeader := true units := "MM" isNPTH := false // Format spec formatDec := 0 // Pending aperture function for the next tool definition pendingType := DrillTypeUnknown scanner := bufio.NewScanner(file) reToolDef := regexp.MustCompile(`^T(\d+)C([\d.]+)`) reToolSelect := regexp.MustCompile(`^T(\d+)$`) reCoord := regexp.MustCompile(`^X([+-]?[\d.]+)Y([+-]?[\d.]+)`) reAperFunc := regexp.MustCompile(`TA\.AperFunction,(\w+),(\w+),(\w+)`) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } // Check file function for NPTH if strings.Contains(line, "TF.FileFunction") { if strings.Contains(line, "NonPlated") || strings.Contains(line, "NPTH") { isNPTH = true } } // Parse TA.AperFunction comments (appears before tool definition) if strings.HasPrefix(line, ";") || strings.HasPrefix(line, "G04") { m := reAperFunc.FindStringSubmatch(line) if len(m) >= 4 { funcType := m[3] switch funcType { case "ViaDrill": pendingType = DrillTypeVia case "ComponentDrill": pendingType = DrillTypeComponent default: pendingType = DrillTypeUnknown } } // Also check for format spec if strings.HasPrefix(line, ";FORMAT=") { re := regexp.MustCompile(`\{(\d+):(\d+)\}`) fm := re.FindStringSubmatch(line) if len(fm) == 3 { formatDec, _ = strconv.Atoi(fm[2]) } } continue } // Detect header end if line == "%" || line == "M95" { inHeader = false continue } // Units if strings.Contains(line, "METRIC") || line == "M71" { units = "MM" continue } if strings.Contains(line, "INCH") || line == "M72" { units = "IN" continue } // Tool definitions (in header): T01C0.300 if inHeader { m := reToolDef.FindStringSubmatch(line) if len(m) == 3 { toolNum, _ := strconv.Atoi(m[1]) dia, _ := strconv.ParseFloat(m[2], 64) ht := pendingType // If this is an NPTH file and type is unknown, classify as mounting if isNPTH && ht == DrillTypeUnknown { ht = DrillTypeMounting } tools[toolNum] = toolInfo{diameter: dia, holeType: ht} pendingType = DrillTypeUnknown // Reset continue } } // Tool selection: T01 m := reToolSelect.FindStringSubmatch(line) if len(m) == 2 { toolNum, _ := strconv.Atoi(m[1]) currentTool = toolNum continue } // End of file if line == "M30" || line == "M00" { break } // Coordinate: X123456Y789012 mc := reCoord.FindStringSubmatch(line) if len(mc) == 3 && currentTool != 0 { x := parseExcellonCoord(mc[1], formatDec) y := parseExcellonCoord(mc[2], formatDec) ti := tools[currentTool] dia := ti.diameter // Convert inches to mm if needed if units == "IN" { x *= 25.4 y *= 25.4 if dia < 1.0 { dia *= 25.4 } } holes = append(holes, DrillHole{ X: x, Y: y, Diameter: dia, Type: ti.holeType, ToolNum: currentTool, }) } } return holes, nil } func parseExcellonCoord(s string, fmtDec int) float64 { if strings.Contains(s, ".") { val, _ := strconv.ParseFloat(s, 64) return val } val, _ := strconv.ParseFloat(s, 64) if fmtDec > 0 { return val / math.Pow(10, float64(fmtDec)) } return val / 1000.0 }