Former/drill.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
}