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") { // We don't have perfect analytic translation to SVG path for region arcs yet. // We can just output the line for now, or approximate it as before. // For SVG, we can just output line segments just like we did for image processing. iVal, jVal := 0.0, 0.0 if cmd.I != nil { iVal = *cmd.I } if cmd.J != nil { jVal = *cmd.J } centerX, centerY := prevX+iVal, 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 * 10) // 10 segments per mm if steps < 10 { steps = 10 } for s := 1; s <= steps; s++ { t := float64(s) / float64(steps) a := startAngle + t*(endAngle-startAngle) ax, ay := centerX+radius*math.Cos(a), centerY+radius*math.Sin(a) regionVertices = append(regionVertices, [2]float64{ax, ay}) } } 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 rx, ry := math.Sqrt(iVal*iVal+jVal*jVal), math.Sqrt(iVal*iVal+jVal*jVal) sweep := 1 // G03 CCW -> SVG path sweep up due to inverted Y if interpolationMode == "G02" { sweep = 0 } fmt.Fprintf(f, ``+"\n", toSVGX(prevX), toSVGY(prevY), rx, ry, 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") } } }