190 lines
4.3 KiB
Go
190 lines
4.3 KiB
Go
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
|
|
}
|