package main import ( "fmt" "math" "os" ) func WriteSVG(filename string, gf *GerberFile, bounds *Bounds) error { b := *bounds widthMM := b.MaxX - b.MinX heightMM := b.MaxY - b.MinY f, err := os.Create(filename) if err != nil { return err } defer f.Close() // Use mm directly for SVG fmt.Fprintf(f, ``, widthMM, heightMM, widthMM, heightMM) fmt.Fprintf(f, "\n\n") // Note: SVG Y-axis points down. We need to invert Y: (heightMM - (y - b.MinY)) toSVGX := func(x float64) float64 { return x - b.MinX } toSVGY := func(y float64) float64 { return heightMM - (y - b.MinY) } curX, curY := 0.0, 0.0 curDCode := 0 interpolationMode := "G01" // Default linear inRegion := false var regionVertices [][2]float64 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" { if len(regionVertices) >= 3 { fmt.Fprintf(f, `\n") } inRegion = false regionVertices = nil continue } prevX, prevY := curX, curY if cmd.X != nil { curX = *cmd.X } if cmd.Y != nil { curY = *cmd.Y } if inRegion { if cmd.Type == "MOVE" || (cmd.Type == "DRAW" && interpolationMode == "G01") { regionVertices = append(regionVertices, [2]float64{curX, curY}) } else if cmd.Type == "DRAW" && (interpolationMode == "G02" || interpolationMode == "G03") { iVal, jVal := 0.0, 0.0 if cmd.I != nil { iVal = *cmd.I } if cmd.J != nil { jVal = *cmd.J } arcPts := approximateArc(prevX, prevY, curX, curY, iVal, jVal, interpolationMode) for _, pt := range arcPts { regionVertices = append(regionVertices, pt) } } continue } if cmd.Type == "FLASH" { ap, ok := gf.State.Apertures[curDCode] if ok { writeSVGAperture(f, toSVGX(curX), toSVGY(curY), ap, false) } } else if cmd.Type == "DRAW" { ap, ok := gf.State.Apertures[curDCode] if ok { // Basic stroke representation for lines w := 0.1 // default if len(ap.Modifiers) > 0 { w = ap.Modifiers[0] } if interpolationMode == "G01" { fmt.Fprintf(f, ``+"\n", toSVGX(prevX), toSVGY(prevY), toSVGX(curX), toSVGY(curY), w) } else { iVal, jVal := 0.0, 0.0 if cmd.I != nil { iVal = *cmd.I } if cmd.J != nil { jVal = *cmd.J } // SVG path arc (Y-axis inverted: G02 CW -> CCW in SVG, G03 CCW -> CW in SVG) r := math.Sqrt(iVal*iVal + jVal*jVal) acx, acy := prevX+iVal, prevY+jVal sa := math.Atan2(prevY-acy, prevX-acx) ea := math.Atan2(curY-acy, curX-acx) var arcSpan float64 if interpolationMode == "G03" { if ea <= sa { ea += 2 * math.Pi } arcSpan = ea - sa } else { if sa <= ea { sa += 2 * math.Pi } arcSpan = sa - ea } largeArc := 0 if arcSpan > math.Pi { largeArc = 1 } sweep := 1 // G03 CCW Gerber -> CW SVG if interpolationMode == "G02" { sweep = 0 } fmt.Fprintf(f, `` + "\n", toSVGX(prevX), toSVGY(prevY), r, r, largeArc, sweep, toSVGX(curX), toSVGY(curY), w) } } } } fmt.Fprintf(f, "\n\n") return nil } func writeSVGAperture(f *os.File, cx, cy float64, ap Aperture, isMacro bool) { switch ap.Type { case "C": if len(ap.Modifiers) > 0 { r := ap.Modifiers[0] / 2 fmt.Fprintf(f, ``+"\n", cx, cy, r) } case "R": if len(ap.Modifiers) >= 2 { w, h := ap.Modifiers[0], ap.Modifiers[1] fmt.Fprintf(f, ``+"\n", cx-w/2, cy-h/2, w, h) } case "O": if len(ap.Modifiers) >= 2 { w, h := ap.Modifiers[0], ap.Modifiers[1] r := w / 2 if h < w { r = h / 2 } fmt.Fprintf(f, ``+"\n", cx-w/2, cy-h/2, w, h, r, r) } case "P": if len(ap.Modifiers) >= 2 { dia, numV := ap.Modifiers[0], int(ap.Modifiers[1]) r := dia / 2 rot := 0.0 if len(ap.Modifiers) >= 3 { rot = ap.Modifiers[2] } fmt.Fprintf(f, ``+"\n") } } }