pcb-to-stencil/gerber.go

1005 lines
27 KiB
Go

package main
import (
"bufio"
"image"
"image/color"
"image/draw"
"math"
"os"
"regexp"
"strconv"
"strings"
)
// Aperture types
const (
ApertureCircle = "C"
ApertureRect = "R"
ApertureObround = "O"
AperturePolygon = "P"
)
type Aperture struct {
Type string
Modifiers []float64
}
type MacroPrimitive struct {
Code int
Modifiers []string // Store as strings to handle $1, $2, expressions like $1+$1
}
type Macro struct {
Name string
Primitives []MacroPrimitive
}
type GerberState struct {
Apertures map[int]Aperture
Macros map[string]Macro
CurrentAperture int
X, Y float64 // Current coordinates in mm
FormatX, FormatY struct {
Integer, Decimal int
}
Units string // "MM" or "IN"
CurrentFootprint string // Stored from %TO.C,Footprint,...*%
}
type GerberCommand struct {
Type string // "D01", "D02", "D03", "AD", "FS", etc.
X, Y *float64
I, J *float64
D *int
Footprint string
}
type GerberFile struct {
Commands []GerberCommand
State GerberState
}
// Footprint represents a component bounding area deduced from Gerber X2 attributes
type Footprint struct {
Name string `json:"name"`
MinX float64 `json:"minX"`
MinY float64 `json:"minY"`
MaxX float64 `json:"maxX"`
MaxY float64 `json:"maxY"`
CenterX float64 `json:"centerX"`
CenterY float64 `json:"centerY"`
}
func ExtractFootprints(gf *GerberFile) []Footprint {
fps := make(map[string]*Footprint)
for _, cmd := range gf.Commands {
if cmd.Footprint == "" {
continue
}
if cmd.X == nil || cmd.Y == nil {
continue
}
fp, exists := fps[cmd.Footprint]
if !exists {
fp = &Footprint{
Name: cmd.Footprint,
MinX: *cmd.X,
MaxX: *cmd.X,
MinY: *cmd.Y,
MaxY: *cmd.Y,
}
fps[cmd.Footprint] = fp
} else {
if *cmd.X < fp.MinX {
fp.MinX = *cmd.X
}
if *cmd.X > fp.MaxX {
fp.MaxX = *cmd.X
}
if *cmd.Y < fp.MinY {
fp.MinY = *cmd.Y
}
if *cmd.Y > fp.MaxY {
fp.MaxY = *cmd.Y
}
}
}
var result []Footprint
for _, fp := range fps {
fp.CenterX = (fp.MinX + fp.MaxX) / 2.0
fp.CenterY = (fp.MinY + fp.MaxY) / 2.0
result = append(result, *fp)
}
return result
}
func NewGerberFile() *GerberFile {
return &GerberFile{
State: GerberState{
Apertures: make(map[int]Aperture),
Macros: make(map[string]Macro),
Units: "MM", // Default, usually set by MO
},
}
}
// ParseGerber parses a simple RS-274X file
func ParseGerber(filename string) (*GerberFile, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
gf := NewGerberFile()
scanner := bufio.NewScanner(file)
// Regex for coordinates: X123Y456D01
reCoord := regexp.MustCompile(`([XYDIJ])([\d\.\-]+)`)
// Regex for Aperture Definition: %ADD10C,0.5*% or %ADD10RoundRect,0.25X-0.75X...*%
reAD := regexp.MustCompile(`%ADD(\d+)([A-Za-z0-9_]+),?([\d\.\-X]+)?\*%`)
// Regex for Format Spec: %FSLAX24Y24*%
reFS := regexp.MustCompile(`%FSLAX(\d)(\d)Y(\d)(\d)\*%`)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
// Handle Parameters
if strings.HasPrefix(line, "%") {
if strings.HasPrefix(line, "%FS") {
matches := reFS.FindStringSubmatch(line)
if len(matches) == 5 {
gf.State.FormatX.Integer, _ = strconv.Atoi(matches[1])
gf.State.FormatX.Decimal, _ = strconv.Atoi(matches[2])
gf.State.FormatY.Integer, _ = strconv.Atoi(matches[3])
gf.State.FormatY.Decimal, _ = strconv.Atoi(matches[4])
}
} else if strings.HasPrefix(line, "%AD") {
matches := reAD.FindStringSubmatch(line)
if len(matches) >= 3 {
dCode, _ := strconv.Atoi(matches[1])
apType := matches[2]
var mods []float64
if len(matches) > 3 && matches[3] != "" {
parts := strings.Split(matches[3], "X")
for _, p := range parts {
val, _ := strconv.ParseFloat(p, 64)
mods = append(mods, val)
}
}
gf.State.Apertures[dCode] = Aperture{Type: apType, Modifiers: mods}
}
} else if strings.HasPrefix(line, "%AM") {
// Parse Macro
name := strings.TrimPrefix(line, "%AM")
name = strings.TrimSuffix(name, "*")
var primitives []MacroPrimitive
for scanner.Scan() {
mLine := strings.TrimSpace(scanner.Text())
// Skip comment lines (start with "0 ")
if strings.HasPrefix(mLine, "0 ") {
continue
}
// Skip empty lines
if mLine == "" {
continue
}
// Check if this line ends the macro definition
// Standard allows ending with *% at end of last primitive OR a separate line with %
trimmedLine := strings.TrimSpace(mLine)
if trimmedLine == "%" {
break
}
endsWithPercent := strings.HasSuffix(mLine, "*%")
// Remove trailing *% or just *
mLine = strings.TrimSuffix(mLine, "*%")
mLine = strings.TrimSuffix(mLine, "*")
// Parse primitive
parts := strings.Split(mLine, ",")
if len(parts) > 0 && parts[0] != "" {
code, err := strconv.Atoi(parts[0])
if err == nil && code > 0 {
// Store modifiers as strings to handle $1, $2, expressions
var mods []string
for _, p := range parts[1:] {
mods = append(mods, strings.TrimSpace(p))
}
primitives = append(primitives, MacroPrimitive{Code: code, Modifiers: mods})
}
}
// If line ended with *%, macro definition is complete
if endsWithPercent {
break
}
}
gf.State.Macros[name] = Macro{Name: name, Primitives: primitives}
} else if strings.HasPrefix(line, "%MO") {
if strings.Contains(line, "IN") {
gf.State.Units = "IN"
} else {
gf.State.Units = "MM"
}
} else if strings.HasPrefix(line, "%TO") {
parts := strings.Split(line, ",")
if len(parts) >= 2 && strings.HasPrefix(parts[0], "%TO.C") {
refDes := strings.TrimSuffix(parts[1], "*%")
if refDes != "" {
gf.State.CurrentFootprint = refDes
}
}
} else if strings.HasPrefix(line, "%TD") {
if strings.Contains(line, "%TD*%") || strings.Contains(line, "%TD,C*%") || strings.Contains(line, "%TD,Footprint*%") {
gf.State.CurrentFootprint = ""
}
}
continue
}
// Handle Standard Commands
// Split by *
parts := strings.Split(line, "*")
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
// Check for G-codes
if strings.HasPrefix(part, "G") {
if part == "G01" {
// Linear interpolation (default)
gf.Commands = append(gf.Commands, GerberCommand{Type: "G01"})
} else if part == "G02" {
// Clockwise circular interpolation
gf.Commands = append(gf.Commands, GerberCommand{Type: "G02"})
} else if part == "G03" {
// Counter-clockwise circular interpolation
gf.Commands = append(gf.Commands, GerberCommand{Type: "G03"})
} else if part == "G36" {
// Region fill start
gf.Commands = append(gf.Commands, GerberCommand{Type: "G36"})
} else if part == "G37" {
// Region fill end
gf.Commands = append(gf.Commands, GerberCommand{Type: "G37"})
}
continue
}
// Handle Aperture Selection (e.g., D10*)
if strings.HasPrefix(part, "D") && len(part) >= 2 {
// Likely D10, D11 etc.
dCode, err := strconv.Atoi(part[1:])
if err == nil && dCode >= 10 {
gf.Commands = append(gf.Commands, GerberCommand{Type: "APERTURE", D: &dCode})
continue
}
}
// Handle Coordinates and Draw/Flash commands
// X...Y...D01*
matches := reCoord.FindAllStringSubmatch(part, -1)
if len(matches) > 0 {
cmd := GerberCommand{Type: "MOVE", Footprint: gf.State.CurrentFootprint}
for _, m := range matches {
valStr := m[2]
switch m[1] {
case "X":
v := gf.parseCoordinate(valStr, gf.State.FormatX)
cmd.X = &v
case "Y":
v := gf.parseCoordinate(valStr, gf.State.FormatY)
cmd.Y = &v
case "I":
v := gf.parseCoordinate(valStr, gf.State.FormatX)
cmd.I = &v
case "J":
v := gf.parseCoordinate(valStr, gf.State.FormatY)
cmd.J = &v
case "D":
val, _ := strconv.ParseFloat(valStr, 64)
d := int(val)
cmd.D = &d
if d == 1 {
cmd.Type = "DRAW"
} else if d == 2 {
cmd.Type = "MOVE"
} else if d == 3 {
cmd.Type = "FLASH"
}
}
}
gf.Commands = append(gf.Commands, cmd)
}
}
}
return gf, nil
}
func (gf *GerberFile) parseCoordinate(valStr string, fmtSpec struct{ Integer, Decimal int }) float64 {
if strings.Contains(valStr, ".") {
val, _ := strconv.ParseFloat(valStr, 64)
return val
}
val, _ := strconv.ParseFloat(valStr, 64)
divisor := math.Pow(10, float64(fmtSpec.Decimal))
return val / divisor
}
// evaluateMacroExpression evaluates a macro expression like "$1", "$1+$1", "0.5", etc.
// with variable substitution from aperture modifiers
func evaluateMacroExpression(expr string, params []float64) float64 {
expr = strings.TrimSpace(expr)
// Handle simple addition (e.g., "$1+$1")
if strings.Contains(expr, "+") {
parts := strings.Split(expr, "+")
result := 0.0
for _, part := range parts {
result += evaluateMacroExpression(strings.TrimSpace(part), params)
}
return result
}
// Handle simple subtraction (e.g., "$1-$2")
if strings.Contains(expr, "-") && !strings.HasPrefix(expr, "-") {
parts := strings.Split(expr, "-")
if len(parts) == 2 {
left := evaluateMacroExpression(strings.TrimSpace(parts[0]), params)
right := evaluateMacroExpression(strings.TrimSpace(parts[1]), params)
return left - right
}
}
// Handle variable substitution (e.g., "$1", "$2")
if strings.HasPrefix(expr, "$") {
varNum, err := strconv.Atoi(expr[1:])
if err == nil && varNum > 0 && varNum <= len(params) {
return params[varNum-1]
}
return 0.0
}
// Handle literal numbers
val, _ := strconv.ParseFloat(expr, 64)
return val
}
// rotatePoint rotates a point (x, y) around the origin by angleDegrees
func rotatePoint(x, y, angleDegrees float64) (float64, float64) {
if angleDegrees == 0 {
return x, y
}
angleRad := angleDegrees * math.Pi / 180.0
cosA := math.Cos(angleRad)
sinA := math.Sin(angleRad)
return x*cosA - y*sinA, x*sinA + y*cosA
}
type Bounds struct {
MinX, MinY, MaxX, MaxY float64
}
func (gf *GerberFile) CalculateBounds() Bounds {
minX, minY := 1e9, 1e9
maxX, maxY := -1e9, -1e9
updateBounds := func(x, y float64) {
if x < minX {
minX = x
}
if y < minY {
minY = y
}
if x > maxX {
maxX = x
}
if y > maxY {
maxY = y
}
}
curX, curY := 0.0, 0.0
for _, cmd := range gf.Commands {
prevX, prevY := curX, curY
if cmd.X != nil {
curX = *cmd.X
}
if cmd.Y != nil {
curY = *cmd.Y
}
if cmd.Type == "FLASH" {
updateBounds(curX, curY)
} else if cmd.Type == "DRAW" {
updateBounds(prevX, prevY)
updateBounds(curX, curY)
}
}
if minX == 1e9 {
// No drawing commands found, default to 0,0
minX, minY = 0, 0
maxX, maxY = 10, 10 // Arbitrary small size
}
// Add some padding
padding := 2.0 // mm
minX -= padding
minY -= padding
maxX += padding
maxY += padding
return Bounds{MinX: minX, MinY: minY, MaxX: maxX, MaxY: maxY}
}
// Render generates an image from the parsed Gerber commands
func (gf *GerberFile) Render(dpi float64, bounds *Bounds) image.Image {
var b Bounds
if bounds != nil {
b = *bounds
} else {
b = gf.CalculateBounds()
}
widthMM := b.MaxX - b.MinX
heightMM := b.MaxY - b.MinY
var scale float64
if gf.State.Units == "IN" {
scale = dpi
} else {
scale = dpi / 25.4
}
imgWidth := int(widthMM * scale)
imgHeight := int(heightMM * scale)
img := image.NewRGBA(image.Rect(0, 0, imgWidth, imgHeight))
// Fill black (stencil material)
draw.Draw(img, img.Bounds(), &image.Uniform{color.Black}, image.Point{}, draw.Src)
// White for holes
white := &image.Uniform{color.White}
// Helper to convert mm to pixels
toPix := func(x, y float64) (int, int) {
px := int((x - b.MinX) * scale)
py := int((heightMM - (y - b.MinY)) * scale) // Flip Y for image coords
return px, py
}
curX, curY := 0.0, 0.0
curDCode := 0
interpolationMode := "G01" // Default linear
inRegion := false
var regionVertices [][2]int
for _, cmd := range gf.Commands {
if cmd.Type == "APERTURE" {
curDCode = *cmd.D
continue
}
if cmd.Type == "G01" || cmd.Type == "G02" || cmd.Type == "G03" {
interpolationMode = cmd.Type
continue
}
if cmd.Type == "G36" {
inRegion = true
regionVertices = nil
continue
}
if cmd.Type == "G37" {
// End region: fill the collected polygon
if len(regionVertices) >= 3 {
drawFilledPolygon(img, regionVertices)
}
inRegion = false
regionVertices = nil
continue
}
prevX, prevY := curX, curY
if cmd.X != nil {
curX = *cmd.X
}
if cmd.Y != nil {
curY = *cmd.Y
}
// In region mode, collect contour vertices instead of drawing
if inRegion {
if cmd.Type == "MOVE" {
// D02 in region: start a new contour
px, py := toPix(curX, curY)
regionVertices = append(regionVertices, [2]int{px, py})
} else if cmd.Type == "DRAW" {
if interpolationMode == "G01" {
// Linear segment: add endpoint
px, py := toPix(curX, curY)
regionVertices = append(regionVertices, [2]int{px, py})
} else {
// Arc segment: sample points along the arc
iVal := 0.0
jVal := 0.0
if cmd.I != nil {
iVal = *cmd.I
}
if cmd.J != nil {
jVal = *cmd.J
}
centerX := prevX + iVal
centerY := prevY + jVal
radius := math.Sqrt(iVal*iVal + jVal*jVal)
startAngle := math.Atan2(prevY-centerY, prevX-centerX)
endAngle := math.Atan2(curY-centerY, curX-centerX)
if interpolationMode == "G03" {
if endAngle <= startAngle {
endAngle += 2 * math.Pi
}
} else {
if startAngle <= endAngle {
startAngle += 2 * math.Pi
}
}
arcLen := math.Abs(endAngle-startAngle) * radius
steps := int(arcLen * scale * 2)
if steps < 10 {
steps = 10
}
for s := 1; s <= steps; s++ {
t := float64(s) / float64(steps)
angle := startAngle + t*(endAngle-startAngle)
ax := centerX + radius*math.Cos(angle)
ay := centerY + radius*math.Sin(angle)
px, py := toPix(ax, ay)
regionVertices = append(regionVertices, [2]int{px, py})
}
}
}
continue
}
if cmd.Type == "FLASH" {
// Draw Aperture at curX, curY
ap, ok := gf.State.Apertures[curDCode]
if ok {
cx, cy := toPix(curX, curY)
gf.drawAperture(img, cx, cy, ap, scale, white)
}
} else if cmd.Type == "DRAW" {
ap, ok := gf.State.Apertures[curDCode]
if ok {
if interpolationMode == "G01" {
// Linear
x1, y1 := toPix(prevX, prevY)
x2, y2 := toPix(curX, curY)
gf.drawLine(img, x1, y1, x2, y2, ap, scale, white)
} else {
// Circular Interpolation (G02/G03)
iVal := 0.0
jVal := 0.0
if cmd.I != nil {
iVal = *cmd.I
}
if cmd.J != nil {
jVal = *cmd.J
}
centerX := prevX + iVal
centerY := prevY + jVal
radius := math.Sqrt(iVal*iVal + jVal*jVal)
startAngle := math.Atan2(prevY-centerY, prevX-centerX)
endAngle := math.Atan2(curY-centerY, curX-centerX)
// Adjust angles for G02 (CW) vs G03 (CCW)
if interpolationMode == "G03" { // CCW
if endAngle <= startAngle {
endAngle += 2 * math.Pi
}
} else { // G02 CW
if startAngle <= endAngle {
startAngle += 2 * math.Pi
}
}
// Arc length approximation
arcLen := math.Abs(endAngle-startAngle) * radius
steps := int(arcLen * scale * 2) // 2x pixel density for smoothness
if steps < 10 {
steps = 10
}
for s := 0; s <= steps; s++ {
t := float64(s) / float64(steps)
angle := startAngle + t*(endAngle-startAngle)
px := centerX + radius*math.Cos(angle)
py := centerY + radius*math.Sin(angle)
ix, iy := toPix(px, py)
gf.drawAperture(img, ix, iy, ap, scale, white)
}
}
}
}
}
return img
}
func (gf *GerberFile) drawAperture(img *image.RGBA, x, y int, ap Aperture, scale float64, c image.Image) {
switch ap.Type {
case ApertureCircle: // C
// Modifiers[0] is diameter
if len(ap.Modifiers) > 0 {
radius := int((ap.Modifiers[0] * scale) / 2)
drawCircle(img, x, y, radius)
}
return
case ApertureRect: // R
// Modifiers[0] is width, [1] is height
if len(ap.Modifiers) >= 2 {
w := int(ap.Modifiers[0] * scale)
h := int(ap.Modifiers[1] * scale)
r := image.Rect(x-w/2, y-h/2, x+w/2, y+h/2)
draw.Draw(img, r, c, image.Point{}, draw.Src)
}
return
case ApertureObround: // O
// Modifiers[0] is width, [1] is height
if len(ap.Modifiers) >= 2 {
w := int(ap.Modifiers[0] * scale)
h := int(ap.Modifiers[1] * scale)
// Draw Obround
// If w > h: Horizontal Pill. Central Rect is (w-h) x h. Two circles of dia h.
// If h > w: Vertical Pill. Central Rect is w x (h-w). Two circles of dia w.
// If w == h: Circle dia w.
drawObround := func(target *image.RGBA, x, y, w, h int, color image.Image) {
if w == h {
radius := w / 2
drawCircle(target, x, y, radius)
return
}
if w > h {
// Horizontal
rectW := w - h
if rectW < 0 {
rectW = 0
} // Should be impossible if w > h
// Center Rect
r := image.Rect(x-rectW/2, y-h/2, x+rectW/2, y+h/2)
draw.Draw(target, r, color, image.Point{}, draw.Src)
// Left Circle
drawCircle(target, x-rectW/2, y, h/2)
// Right Circle
drawCircle(target, x+rectW/2, y, h/2)
} else {
// Vertical
rectH := h - w
if rectH < 0 {
rectH = 0
}
// Center Rect
r := image.Rect(x-w/2, y-rectH/2, x+w/2, y+rectH/2)
draw.Draw(target, r, color, image.Point{}, draw.Src)
// Top Circle (Y decreases upwards in image coords usually, but here we treat y as center)
// Note: In our coordinate system, y is center.
drawCircle(target, x, y-rectH/2, w/2)
// Bottom Circle
drawCircle(target, x, y+rectH/2, w/2)
}
}
drawObround(img, x, y, w, h, c)
}
return
case AperturePolygon: // P
// Modifiers: [0] diameter, [1] vertices, [2] rotation (optional)
if len(ap.Modifiers) >= 2 {
diameter := ap.Modifiers[0]
numVertices := int(ap.Modifiers[1])
rotation := 0.0
if len(ap.Modifiers) >= 3 {
rotation = ap.Modifiers[2]
}
if numVertices >= 3 {
radius := (diameter * scale) / 2
vertices := make([][2]int, numVertices)
for i := 0; i < numVertices; i++ {
angleDeg := rotation + float64(i)*360.0/float64(numVertices)
angleRad := angleDeg * math.Pi / 180.0
px := int(radius * math.Cos(angleRad))
py := int(radius * math.Sin(angleRad))
vertices[i] = [2]int{x + px, y - py}
}
drawFilledPolygon(img, vertices)
}
}
return
}
// Check for Macros
if macro, ok := gf.State.Macros[ap.Type]; ok {
for _, prim := range macro.Primitives {
switch prim.Code {
case 1: // Circle
// Mods: Exposure, Diameter, CenterX, CenterY
if len(prim.Modifiers) >= 4 {
exposure := evaluateMacroExpression(prim.Modifiers[0], ap.Modifiers)
if exposure == 0 {
break
}
dia := evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers)
cx := evaluateMacroExpression(prim.Modifiers[2], ap.Modifiers)
cy := evaluateMacroExpression(prim.Modifiers[3], ap.Modifiers)
px := int(cx * scale)
py := int(cy * scale)
radius := int((dia * scale) / 2)
drawCircle(img, x+px, y-py, radius)
}
case 4: // Outline (Polygon)
// Mods: Exposure, NumVertices, X1, Y1, ..., Xn, Yn, Rotation
if len(prim.Modifiers) >= 3 {
exposure := evaluateMacroExpression(prim.Modifiers[0], ap.Modifiers)
if exposure == 0 {
// Skip if exposure is off
break
}
numVertices := int(evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers))
// Need at least numVertices * 2 coordinates + rotation
if len(prim.Modifiers) >= 2+numVertices*2+1 {
rotation := evaluateMacroExpression(prim.Modifiers[2+numVertices*2], ap.Modifiers)
// Extract vertices
vertices := make([][2]int, numVertices)
for i := 0; i < numVertices; i++ {
vx := evaluateMacroExpression(prim.Modifiers[2+i*2], ap.Modifiers)
vy := evaluateMacroExpression(prim.Modifiers[2+i*2+1], ap.Modifiers)
// Apply rotation
vx, vy = rotatePoint(vx, vy, rotation)
px := int(vx * scale)
py := int(vy * scale)
vertices[i] = [2]int{x + px, y - py}
}
// Draw filled polygon using scanline algorithm
drawFilledPolygon(img, vertices)
}
}
case 5: // Regular Polygon
// Mods: Exposure, NumVertices, CenterX, CenterY, Diameter, Rotation
if len(prim.Modifiers) >= 6 {
exposure := evaluateMacroExpression(prim.Modifiers[0], ap.Modifiers)
if exposure == 0 {
break
}
numVertices := int(evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers))
if numVertices >= 3 {
cx := evaluateMacroExpression(prim.Modifiers[2], ap.Modifiers)
cy := evaluateMacroExpression(prim.Modifiers[3], ap.Modifiers)
diameter := evaluateMacroExpression(prim.Modifiers[4], ap.Modifiers)
rotation := evaluateMacroExpression(prim.Modifiers[5], ap.Modifiers)
radius := (diameter * scale) / 2
pxCenter := int(cx * scale)
pyCenter := int(cy * scale)
vertices := make([][2]int, numVertices)
for i := 0; i < numVertices; i++ {
angleDeg := rotation + float64(i)*360.0/float64(numVertices)
angleRad := angleDeg * math.Pi / 180.0
px := int(radius * math.Cos(angleRad))
py := int(radius * math.Sin(angleRad))
vertices[i] = [2]int{x + pxCenter + px, y - pyCenter - py}
}
drawFilledPolygon(img, vertices)
}
}
case 20: // Vector Line
// Mods: Exposure, Width, StartX, StartY, EndX, EndY, Rotation
if len(prim.Modifiers) >= 7 {
exposure := evaluateMacroExpression(prim.Modifiers[0], ap.Modifiers)
if exposure == 0 {
// Skip if exposure is off
break
}
width := evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers)
startX := evaluateMacroExpression(prim.Modifiers[2], ap.Modifiers)
startY := evaluateMacroExpression(prim.Modifiers[3], ap.Modifiers)
endX := evaluateMacroExpression(prim.Modifiers[4], ap.Modifiers)
endY := evaluateMacroExpression(prim.Modifiers[5], ap.Modifiers)
rotation := evaluateMacroExpression(prim.Modifiers[6], ap.Modifiers)
// Apply rotation to start and end points
startX, startY = rotatePoint(startX, startY, rotation)
endX, endY = rotatePoint(endX, endY, rotation)
// Calculate the rectangle representing the line
// The line goes from (startX, startY) to (endX, endY) with width
dx := endX - startX
dy := endY - startY
length := math.Sqrt(dx*dx + dy*dy)
if length > 0 {
// Perpendicular vector for width
perpX := -dy / length * width / 2
perpY := dx / length * width / 2
// Four corners of the rectangle
vertices := [][2]int{
{x + int((startX-perpX)*scale), y - int((startY-perpY)*scale)},
{x + int((startX+perpX)*scale), y - int((startY+perpY)*scale)},
{x + int((endX+perpX)*scale), y - int((endY+perpY)*scale)},
{x + int((endX-perpX)*scale), y - int((endY-perpY)*scale)},
}
drawFilledPolygon(img, vertices)
}
}
case 21: // Center Line (Rect)
// Mods: Exposure, Width, Height, CenterX, CenterY, Rotation
if len(prim.Modifiers) >= 6 {
exposure := evaluateMacroExpression(prim.Modifiers[0], ap.Modifiers)
if exposure == 0 {
break
}
width := evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers)
height := evaluateMacroExpression(prim.Modifiers[2], ap.Modifiers)
cx := evaluateMacroExpression(prim.Modifiers[3], ap.Modifiers)
cy := evaluateMacroExpression(prim.Modifiers[4], ap.Modifiers)
rot := evaluateMacroExpression(prim.Modifiers[5], ap.Modifiers)
// Calculate the four corners of the rectangle (centered at origin)
halfW := width / 2
halfH := height / 2
// Four corners before rotation
corners := [][2]float64{
{-halfW, -halfH},
{halfW, -halfH},
{halfW, halfH},
{-halfW, halfH},
}
// Apply rotation and translation
vertices := make([][2]int, 4)
for i, corner := range corners {
// Rotate around origin
rx, ry := rotatePoint(corner[0], corner[1], rot)
// Translate to center position
rx += cx
ry += cy
// Convert to pixels
px := int(rx * scale)
py := int(ry * scale)
vertices[i] = [2]int{x + px, y - py}
}
// Draw as polygon
drawFilledPolygon(img, vertices)
}
}
}
}
}
func drawCircle(img *image.RGBA, x0, y0, r int) {
// Simple Bresenham or scanline
for y := -r; y <= r; y++ {
for x := -r; x <= r; x++ {
if x*x+y*y <= r*r {
img.Set(x0+x, y0+y, color.White)
}
}
}
}
func drawFilledPolygon(img *image.RGBA, vertices [][2]int) {
if len(vertices) < 3 {
return
}
// Find bounding box
minY, maxY := vertices[0][1], vertices[0][1]
for _, v := range vertices {
if v[1] < minY {
minY = v[1]
}
if v[1] > maxY {
maxY = v[1]
}
}
// Scanline fill algorithm
for y := minY; y <= maxY; y++ {
// Find intersections with polygon edges
var intersections []int
for i := 0; i < len(vertices); i++ {
j := (i + 1) % len(vertices)
y1, y2 := vertices[i][1], vertices[j][1]
x1, x2 := vertices[i][0], vertices[j][0]
// Check if scanline intersects this edge
if (y1 <= y && y < y2) || (y2 <= y && y < y1) {
// Calculate x intersection
x := x1 + (y-y1)*(x2-x1)/(y2-y1)
intersections = append(intersections, x)
}
}
// Sort intersections
for i := 0; i < len(intersections)-1; i++ {
for j := i + 1; j < len(intersections); j++ {
if intersections[i] > intersections[j] {
intersections[i], intersections[j] = intersections[j], intersections[i]
}
}
}
// Fill between pairs of intersections
for i := 0; i < len(intersections)-1; i += 2 {
x1 := intersections[i]
x2 := intersections[i+1]
for x := x1; x <= x2; x++ {
if x >= 0 && x < img.Bounds().Max.X && y >= 0 && y < img.Bounds().Max.Y {
img.Set(x, y, color.White)
}
}
}
}
}
func (gf *GerberFile) drawLine(img *image.RGBA, x1, y1, x2, y2 int, ap Aperture, scale float64, c image.Image) {
// Bresenham's line algorithm, but we need to stroke it with the aperture.
// For simplicity, if aperture is Circle, we draw a circle at each step (inefficient but works).
// If aperture is Rect, we draw rect at each step.
// Optimized: Just draw a thick line if it's a circle aperture
dx := float64(x2 - x1)
dy := float64(y2 - y1)
dist := math.Sqrt(dx*dx + dy*dy)
steps := int(dist) // 1 pixel steps
if steps == 0 {
gf.drawAperture(img, x1, y1, ap, scale, c)
return
}
for i := 0; i <= steps; i++ {
t := float64(i) / float64(steps)
x := int(float64(x1) + t*dx)
y := int(float64(y1) + t*dy)
gf.drawAperture(img, x, y, ap, scale, c)
}
}