pcb-to-stencil/main.go

192 lines
4.4 KiB
Go

package main
import (
"flag"
"fmt"
"image"
"image/png"
"log"
"os"
"path/filepath"
"strings"
)
// --- Configuration ---
const (
DPI = 1000.0 // Higher DPI = smoother curves
PixelToMM = 25.4 / DPI
)
var StencilHeight float64 = 0.2 // mm, default
var KeepPNG bool
// --- STL Helpers ---
type Point struct {
X, Y, Z float64
}
func WriteSTL(filename string, triangles [][3]Point) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
// Writing Binary STL is harder, ASCII is fine for this size
f.WriteString("solid stencil\n")
for _, t := range triangles {
f.WriteString("facet normal 0 0 0\n")
f.WriteString(" outer loop\n")
for _, p := range t {
f.WriteString(fmt.Sprintf(" vertex %f %f %f\n", p.X, p.Y, p.Z))
}
f.WriteString(" endloop\n")
f.WriteString("endfacet\n")
}
f.WriteString("endsolid stencil\n")
return nil
}
func AddBox(triangles *[][3]Point, x, y, w, h, zHeight float64) {
x0, y0 := x, y
x1, y1 := x+w, y+h
z0, z1 := 0.0, zHeight
p000 := Point{x0, y0, z0}
p100 := Point{x1, y0, z0}
p110 := Point{x1, y1, z0}
p010 := Point{x0, y1, z0}
p001 := Point{x0, y0, z1}
p101 := Point{x1, y0, z1}
p111 := Point{x1, y1, z1}
p011 := Point{x0, y1, z1}
addQuad := func(a, b, c, d Point) {
*triangles = append(*triangles, [3]Point{a, b, c})
*triangles = append(*triangles, [3]Point{c, d, a})
}
addQuad(p000, p010, p110, p100) // Bottom
addQuad(p101, p111, p011, p001) // Top
addQuad(p000, p100, p101, p001) // Front
addQuad(p100, p110, p111, p101) // Right
addQuad(p110, p010, p011, p111) // Back
addQuad(p010, p000, p001, p011) // Left
}
// --- Meshing Logic (Optimized) ---
func GenerateMeshFromImage(img image.Image) [][3]Point {
bounds := img.Bounds()
width := bounds.Max.X
height := bounds.Max.Y
var triangles [][3]Point
// Optimization: Run-Length Encoding
for y := 0; y < height; y++ {
var startX = -1
for x := 0; x < width; x++ {
c := img.At(x, y)
r, g, b, _ := c.RGBA()
// Check for BLACK pixels (The Plastic Stencil Body)
// Adjust threshold if gerbv produces slightly gray blacks
isSolid := r < 10000 && g < 10000 && b < 10000
if isSolid {
if startX == -1 {
startX = x
}
} else {
if startX != -1 {
// End of strip, generate box
stripLen := x - startX
AddBox(
&triangles,
float64(startX)*PixelToMM,
float64(y)*PixelToMM,
float64(stripLen)*PixelToMM,
PixelToMM,
StencilHeight,
)
startX = -1
}
}
}
if startX != -1 {
stripLen := width - startX
AddBox(
&triangles,
float64(startX)*PixelToMM,
float64(y)*PixelToMM,
float64(stripLen)*PixelToMM,
PixelToMM,
StencilHeight,
)
}
}
return triangles
}
// --- Main ---
func main() {
flag.Float64Var(&StencilHeight, "height", 0.2, "Stencil height in mm")
flag.Float64Var(&StencilHeight, "h", 0.2, "Stencil height in mm (short)")
flag.BoolVar(&KeepPNG, "keep-png", false, "Save intermediate PNG file")
flag.BoolVar(&KeepPNG, "kp", false, "Save intermediate PNG file (short)")
flag.Parse()
args := flag.Args()
if len(args) < 1 {
fmt.Println("Usage: go run main.go [options] <path_to_gerber_file>")
fmt.Println("Options:")
flag.PrintDefaults()
fmt.Println("Example: go run main.go -height=0.3 MyPCB.GTP")
os.Exit(1)
}
gerberPath := args[0]
outputPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + ".stl"
// 1. Parse Gerber
fmt.Printf("Parsing %s...\n", gerberPath)
gf, err := ParseGerber(gerberPath)
if err != nil {
log.Fatalf("Error parsing gerber: %v", err)
}
// 2. Render to Image
fmt.Println("Rendering to internal image...")
img := gf.Render(DPI)
if KeepPNG {
pngPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + ".png"
fmt.Printf("Saving intermediate PNG to %s...\n", pngPath)
f, err := os.Create(pngPath)
if err != nil {
log.Printf("Warning: Could not create PNG file: %v", err)
} else {
if err := png.Encode(f, img); err != nil {
log.Printf("Warning: Could not encode PNG: %v", err)
}
f.Close()
}
}
// 3. Generate Mesh
fmt.Println("Generating mesh (this may take 10-20 seconds for large boards)...")
triangles := GenerateMeshFromImage(img)
// 4. Save STL
fmt.Printf("Saving to %s (%d triangles)...\n", outputPath, len(triangles))
err = WriteSTL(outputPath, triangles)
if err != nil {
log.Fatalf("Error writing STL: %v", err)
}
fmt.Println("Success! Happy printing.")
}