outline wall WIP
This commit is contained in:
parent
2d6cf465e3
commit
ec4630c5c4
|
|
@ -1,4 +1,4 @@
|
||||||
# PCB to Stencil Converter
|
# Gerber Solder Paste Layer to Solder Stencil Converter
|
||||||
|
|
||||||
A Go tool to convert Gerber files (specifically solder paste layers) into 3D printable STL stencils.
|
A Go tool to convert Gerber files (specifically solder paste layers) into 3D printable STL stencils.
|
||||||
|
|
||||||
|
|
@ -20,8 +20,10 @@ go run main.go gerber.go [options] <path_to_gerber_file>
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
- `--height, -h`: Stencil height in mm (default: 0.2mm).
|
- `--height`: Stencil height in mm (default: 0.2mm).
|
||||||
- `--keep-png, --kp`: Save the intermediate PNG image used for mesh generation (useful for debugging).
|
- `--wall-height`: Wall height mm (default: 2.0mm).
|
||||||
|
- `--wall-thickness`: Wall thickness in mm (default: 1mm).
|
||||||
|
- `--keep-png`: Save the intermediate PNG image used for mesh generation (useful for debugging).
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
36
gerber.go
36
gerber.go
|
|
@ -219,9 +219,11 @@ func (gf *GerberFile) parseCoordinate(valStr string, fmtSpec struct{ Integer, De
|
||||||
return val / divisor
|
return val / divisor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render generates an image from the parsed Gerber commands
|
type Bounds struct {
|
||||||
func (gf *GerberFile) Render(dpi float64) image.Image {
|
MinX, MinY, MaxX, MaxY float64
|
||||||
// 1. Calculate Bounds
|
}
|
||||||
|
|
||||||
|
func (gf *GerberFile) CalculateBounds() Bounds {
|
||||||
minX, minY := 1e9, 1e9
|
minX, minY := 1e9, 1e9
|
||||||
maxX, maxY := -1e9, -1e9
|
maxX, maxY := -1e9, -1e9
|
||||||
|
|
||||||
|
|
@ -271,8 +273,20 @@ func (gf *GerberFile) Render(dpi float64) image.Image {
|
||||||
maxX += padding
|
maxX += padding
|
||||||
maxY += padding
|
maxY += padding
|
||||||
|
|
||||||
widthMM := maxX - minX
|
return Bounds{MinX: minX, MinY: minY, MaxX: maxX, MaxY: maxY}
|
||||||
heightMM := maxY - minY
|
}
|
||||||
|
|
||||||
|
// 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
|
var scale float64
|
||||||
if gf.State.Units == "IN" {
|
if gf.State.Units == "IN" {
|
||||||
|
|
@ -294,12 +308,12 @@ func (gf *GerberFile) Render(dpi float64) image.Image {
|
||||||
|
|
||||||
// Helper to convert mm to pixels
|
// Helper to convert mm to pixels
|
||||||
toPix := func(x, y float64) (int, int) {
|
toPix := func(x, y float64) (int, int) {
|
||||||
px := int((x - minX) * scale)
|
px := int((x - b.MinX) * scale)
|
||||||
py := int((heightMM - (y - minY)) * scale) // Flip Y for image coords
|
py := int((heightMM - (y - b.MinY)) * scale) // Flip Y for image coords
|
||||||
return px, py
|
return px, py
|
||||||
}
|
}
|
||||||
|
|
||||||
curX, curY = 0.0, 0.0
|
curX, curY := 0.0, 0.0
|
||||||
curDCode := 0
|
curDCode := 0
|
||||||
|
|
||||||
for _, cmd := range gf.Commands {
|
for _, cmd := range gf.Commands {
|
||||||
|
|
@ -343,7 +357,7 @@ func (gf *GerberFile) drawAperture(img *image.RGBA, x, y int, ap Aperture, scale
|
||||||
// Modifiers[0] is diameter
|
// Modifiers[0] is diameter
|
||||||
if len(ap.Modifiers) > 0 {
|
if len(ap.Modifiers) > 0 {
|
||||||
radius := int((ap.Modifiers[0] * scale) / 2)
|
radius := int((ap.Modifiers[0] * scale) / 2)
|
||||||
drawCircle(img, x, y, radius, c)
|
drawCircle(img, x, y, radius)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case ApertureRect: // R
|
case ApertureRect: // R
|
||||||
|
|
@ -383,7 +397,7 @@ func (gf *GerberFile) drawAperture(img *image.RGBA, x, y int, ap Aperture, scale
|
||||||
py := int(cy * scale)
|
py := int(cy * scale)
|
||||||
|
|
||||||
radius := int((dia * scale) / 2)
|
radius := int((dia * scale) / 2)
|
||||||
drawCircle(img, x+px, y-py, radius, c)
|
drawCircle(img, x+px, y-py, radius)
|
||||||
}
|
}
|
||||||
case 21: // Center Line (Rect)
|
case 21: // Center Line (Rect)
|
||||||
// Mods: Exposure, Width, Height, CenterX, CenterY, Rotation
|
// Mods: Exposure, Width, Height, CenterX, CenterY, Rotation
|
||||||
|
|
@ -421,7 +435,7 @@ func (gf *GerberFile) drawAperture(img *image.RGBA, x, y int, ap Aperture, scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawCircle(img *image.RGBA, x0, y0, r int, c image.Image) {
|
func drawCircle(img *image.RGBA, x0, y0, r int) {
|
||||||
// Simple Bresenham or scanline
|
// Simple Bresenham or scanline
|
||||||
for y := -r; y <= r; y++ {
|
for y := -r; y <= r; y++ {
|
||||||
for x := -r; x <= r; x++ {
|
for x := -r; x <= r; x++ {
|
||||||
|
|
|
||||||
427
main.go
427
main.go
|
|
@ -1,23 +1,25 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Configuration ---
|
// --- Configuration ---
|
||||||
const (
|
var DPI float64 = 1000.0 // Higher DPI = smoother curves
|
||||||
DPI = 1000.0 // Higher DPI = smoother curves
|
var PixelToMM float64 = 25.4 / DPI
|
||||||
PixelToMM = 25.4 / DPI
|
|
||||||
)
|
|
||||||
|
|
||||||
var StencilHeight float64 = 0.2 // mm, default
|
var StencilHeight float64 = 0.2 // mm, default
|
||||||
|
var WallHeight float64 = 2.0 // mm, default
|
||||||
|
var WallThickness float64 = 1.0 // mm, default
|
||||||
var KeepPNG bool
|
var KeepPNG bool
|
||||||
|
|
||||||
// --- STL Helpers ---
|
// --- STL Helpers ---
|
||||||
|
|
@ -33,18 +35,58 @@ func WriteSTL(filename string, triangles [][3]Point) error {
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
// Writing Binary STL is harder, ASCII is fine for this size
|
// Write Binary STL Header (80 bytes)
|
||||||
f.WriteString("solid stencil\n")
|
header := make([]byte, 80)
|
||||||
|
copy(header, "Generated by pcb-to-stencil")
|
||||||
|
if _, err := f.Write(header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Number of Triangles (4 bytes uint32)
|
||||||
|
count := uint32(len(triangles))
|
||||||
|
if err := binary.Write(f, binary.LittleEndian, count); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Triangles
|
||||||
|
// Each triangle is 50 bytes:
|
||||||
|
// Normal (3 floats = 12 bytes)
|
||||||
|
// Vertex 1 (3 floats = 12 bytes)
|
||||||
|
// Vertex 2 (3 floats = 12 bytes)
|
||||||
|
// Vertex 3 (3 floats = 12 bytes)
|
||||||
|
// Attribute byte count (2 bytes uint16)
|
||||||
|
|
||||||
|
// Buffer for a single triangle to minimize syscalls
|
||||||
|
buf := make([]byte, 50)
|
||||||
|
|
||||||
for _, t := range triangles {
|
for _, t := range triangles {
|
||||||
f.WriteString("facet normal 0 0 0\n")
|
// Normal (0,0,0)
|
||||||
f.WriteString(" outer loop\n")
|
binary.LittleEndian.PutUint32(buf[0:4], math.Float32bits(0))
|
||||||
for _, p := range t {
|
binary.LittleEndian.PutUint32(buf[4:8], math.Float32bits(0))
|
||||||
f.WriteString(fmt.Sprintf(" vertex %f %f %f\n", p.X, p.Y, p.Z))
|
binary.LittleEndian.PutUint32(buf[8:12], math.Float32bits(0))
|
||||||
|
|
||||||
|
// Vertex 1
|
||||||
|
binary.LittleEndian.PutUint32(buf[12:16], math.Float32bits(float32(t[0].X)))
|
||||||
|
binary.LittleEndian.PutUint32(buf[16:20], math.Float32bits(float32(t[0].Y)))
|
||||||
|
binary.LittleEndian.PutUint32(buf[20:24], math.Float32bits(float32(t[0].Z)))
|
||||||
|
|
||||||
|
// Vertex 2
|
||||||
|
binary.LittleEndian.PutUint32(buf[24:28], math.Float32bits(float32(t[1].X)))
|
||||||
|
binary.LittleEndian.PutUint32(buf[28:32], math.Float32bits(float32(t[1].Y)))
|
||||||
|
binary.LittleEndian.PutUint32(buf[32:36], math.Float32bits(float32(t[1].Z)))
|
||||||
|
|
||||||
|
// Vertex 3
|
||||||
|
binary.LittleEndian.PutUint32(buf[36:40], math.Float32bits(float32(t[2].X)))
|
||||||
|
binary.LittleEndian.PutUint32(buf[40:44], math.Float32bits(float32(t[2].Y)))
|
||||||
|
binary.LittleEndian.PutUint32(buf[44:48], math.Float32bits(float32(t[2].Z)))
|
||||||
|
|
||||||
|
// Attribute byte count (0)
|
||||||
|
binary.LittleEndian.PutUint16(buf[48:50], 0)
|
||||||
|
|
||||||
|
if _, err := f.Write(buf); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
f.WriteString(" endloop\n")
|
|
||||||
f.WriteString("endfacet\n")
|
|
||||||
}
|
}
|
||||||
f.WriteString("endsolid stencil\n")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,27 +119,283 @@ func AddBox(triangles *[][3]Point, x, y, w, h, zHeight float64) {
|
||||||
|
|
||||||
// --- Meshing Logic (Optimized) ---
|
// --- Meshing Logic (Optimized) ---
|
||||||
|
|
||||||
func GenerateMeshFromImage(img image.Image) [][3]Point {
|
// ComputeWallMask generates a mask for the wall based on the outline image.
|
||||||
|
// It identifies the board area (inside the outline) and creates a wall of
|
||||||
|
// specified thickness around it.
|
||||||
|
func ComputeWallMask(img image.Image, thicknessMM float64) ([]bool, []bool) {
|
||||||
bounds := img.Bounds()
|
bounds := img.Bounds()
|
||||||
|
w := bounds.Max.X
|
||||||
|
h := bounds.Max.Y
|
||||||
|
size := w * h
|
||||||
|
|
||||||
|
// Helper for neighbors
|
||||||
|
dx := []int{0, 0, 1, -1}
|
||||||
|
dy := []int{1, -1, 0, 0}
|
||||||
|
|
||||||
|
// 1. Identify Outline Pixels (White)
|
||||||
|
isOutline := make([]bool, size)
|
||||||
|
outlineQueue := []int{}
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
cx := i % w
|
||||||
|
cy := i / w
|
||||||
|
c := img.At(cx, cy)
|
||||||
|
r, _, _, _ := c.RGBA()
|
||||||
|
if r > 10000 { // White-ish
|
||||||
|
isOutline[i] = true
|
||||||
|
outlineQueue = append(outlineQueue, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Dilate Outline to close gaps
|
||||||
|
// We dilate by a small amount (e.g. 0.5mm) to ensure the outline is closed.
|
||||||
|
gapClosingMM := 0.5
|
||||||
|
gapClosingPixels := int(gapClosingMM / PixelToMM)
|
||||||
|
if gapClosingPixels < 1 {
|
||||||
|
gapClosingPixels = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dist := make([]int, size)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
if isOutline[i] {
|
||||||
|
dist[i] = 0
|
||||||
|
} else {
|
||||||
|
dist[i] = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BFS for Dilation
|
||||||
|
dilatedOutline := make([]bool, size)
|
||||||
|
copy(dilatedOutline, isOutline)
|
||||||
|
|
||||||
|
// Use a separate queue for dilation to avoid modifying the original outlineQueue if we needed it
|
||||||
|
dQueue := make([]int, len(outlineQueue))
|
||||||
|
copy(dQueue, outlineQueue)
|
||||||
|
|
||||||
|
for len(dQueue) > 0 {
|
||||||
|
idx := dQueue[0]
|
||||||
|
dQueue = dQueue[1:]
|
||||||
|
|
||||||
|
d := dist[idx]
|
||||||
|
if d >= gapClosingPixels {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cx := idx % w
|
||||||
|
cy := idx / w
|
||||||
|
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
nx, ny := cx+dx[i], cy+dy[i]
|
||||||
|
if nx >= 0 && nx < w && ny >= 0 && ny < h {
|
||||||
|
nIdx := ny*w + nx
|
||||||
|
if dist[nIdx] == -1 {
|
||||||
|
dist[nIdx] = d + 1
|
||||||
|
dilatedOutline[nIdx] = true
|
||||||
|
dQueue = append(dQueue, nIdx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Flood Fill "Outside" using Dilated Outline as barrier
|
||||||
|
isOutside := make([]bool, size)
|
||||||
|
// Start from (0,0) - assumed to be outside due to padding
|
||||||
|
if !dilatedOutline[0] {
|
||||||
|
isOutside[0] = true
|
||||||
|
fQueue := []int{0}
|
||||||
|
|
||||||
|
for len(fQueue) > 0 {
|
||||||
|
idx := fQueue[0]
|
||||||
|
fQueue = fQueue[1:]
|
||||||
|
|
||||||
|
cx := idx % w
|
||||||
|
cy := idx / w
|
||||||
|
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
nx, ny := cx+dx[i], cy+dy[i]
|
||||||
|
if nx >= 0 && nx < w && ny >= 0 && ny < h {
|
||||||
|
nIdx := ny*w + nx
|
||||||
|
if !isOutside[nIdx] && !dilatedOutline[nIdx] {
|
||||||
|
isOutside[nIdx] = true
|
||||||
|
fQueue = append(fQueue, nIdx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Restore Board Shape (Erode "Outside" back to original boundary)
|
||||||
|
// We dilated the outline, so "Outside" stopped 'gapClosingPixels' away from the real board edge.
|
||||||
|
// We need to expand "Outside" inwards by 'gapClosingPixels' to touch the real board edge.
|
||||||
|
// Then "Board" = !Outside.
|
||||||
|
|
||||||
|
// Reset dist for Outside expansion
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
if isOutside[i] {
|
||||||
|
dist[i] = 0
|
||||||
|
} else {
|
||||||
|
dist[i] = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oQueue := []int{}
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
if isOutside[i] {
|
||||||
|
oQueue = append(oQueue, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isOutsideExpanded := make([]bool, size)
|
||||||
|
copy(isOutsideExpanded, isOutside)
|
||||||
|
|
||||||
|
for len(oQueue) > 0 {
|
||||||
|
idx := oQueue[0]
|
||||||
|
oQueue = oQueue[1:]
|
||||||
|
|
||||||
|
d := dist[idx]
|
||||||
|
if d >= gapClosingPixels {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cx := idx % w
|
||||||
|
cy := idx / w
|
||||||
|
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
nx, ny := cx+dx[i], cy+dy[i]
|
||||||
|
if nx >= 0 && nx < w && ny >= 0 && ny < h {
|
||||||
|
nIdx := ny*w + nx
|
||||||
|
if dist[nIdx] == -1 {
|
||||||
|
dist[nIdx] = d + 1
|
||||||
|
isOutsideExpanded[nIdx] = true
|
||||||
|
oQueue = append(oQueue, nIdx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Define Board
|
||||||
|
isBoard := make([]bool, size)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
isBoard[i] = !isOutsideExpanded[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Generate Wall
|
||||||
|
// Wall is generated by expanding Board outwards.
|
||||||
|
// We want the wall to be strictly OUTSIDE the board (or centered on outline? User said "starts at outline").
|
||||||
|
// If we expand Board, we get pixels outside.
|
||||||
|
|
||||||
|
thicknessPixels := int(thicknessMM / PixelToMM)
|
||||||
|
if thicknessPixels < 1 {
|
||||||
|
thicknessPixels = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset dist for Wall generation
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
if isBoard[i] {
|
||||||
|
dist[i] = 0
|
||||||
|
} else {
|
||||||
|
dist[i] = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wQueue := []int{}
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
if isBoard[i] {
|
||||||
|
wQueue = append(wQueue, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isWall := make([]bool, size)
|
||||||
|
|
||||||
|
for len(wQueue) > 0 {
|
||||||
|
idx := wQueue[0]
|
||||||
|
wQueue = wQueue[1:]
|
||||||
|
|
||||||
|
d := dist[idx]
|
||||||
|
if d >= thicknessPixels {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cx := idx % w
|
||||||
|
cy := idx / w
|
||||||
|
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
nx, ny := cx+dx[i], cy+dy[i]
|
||||||
|
if nx >= 0 && nx < w && ny >= 0 && ny < h {
|
||||||
|
nIdx := ny*w + nx
|
||||||
|
if dist[nIdx] == -1 {
|
||||||
|
dist[nIdx] = d + 1
|
||||||
|
isWall[nIdx] = true
|
||||||
|
wQueue = append(wQueue, nIdx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isWall, isBoard
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point {
|
||||||
|
bounds := stencilImg.Bounds()
|
||||||
width := bounds.Max.X
|
width := bounds.Max.X
|
||||||
height := bounds.Max.Y
|
height := bounds.Max.Y
|
||||||
var triangles [][3]Point
|
var triangles [][3]Point
|
||||||
|
|
||||||
|
var wallMask []bool
|
||||||
|
var boardMask []bool
|
||||||
|
if outlineImg != nil {
|
||||||
|
fmt.Println("Computing wall mask...")
|
||||||
|
wallMask, boardMask = ComputeWallMask(outlineImg, WallThickness)
|
||||||
|
}
|
||||||
|
|
||||||
// Optimization: Run-Length Encoding
|
// Optimization: Run-Length Encoding
|
||||||
for y := 0; y < height; y++ {
|
for y := 0; y < height; y++ {
|
||||||
var startX = -1
|
var startX = -1
|
||||||
|
var currentHeight = 0.0
|
||||||
|
|
||||||
for x := 0; x < width; x++ {
|
for x := 0; x < width; x++ {
|
||||||
c := img.At(x, y)
|
// Check stencil (black = solid)
|
||||||
r, g, b, _ := c.RGBA()
|
sc := stencilImg.At(x, y)
|
||||||
|
sr, sg, sb, _ := sc.RGBA()
|
||||||
|
isStencilSolid := sr < 10000 && sg < 10000 && sb < 10000
|
||||||
|
|
||||||
// Check for BLACK pixels (The Plastic Stencil Body)
|
// Check wall
|
||||||
// Adjust threshold if gerbv produces slightly gray blacks
|
isWall := false
|
||||||
isSolid := r < 10000 && g < 10000 && b < 10000
|
isInsideBoard := true
|
||||||
|
if wallMask != nil {
|
||||||
|
idx := y*width + x
|
||||||
|
isWall = wallMask[idx]
|
||||||
|
if boardMask != nil {
|
||||||
|
isInsideBoard = boardMask[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if isSolid {
|
// Determine height at this pixel
|
||||||
|
h := 0.0
|
||||||
|
if isWall {
|
||||||
|
h = WallHeight
|
||||||
|
} else if isStencilSolid {
|
||||||
|
if isInsideBoard {
|
||||||
|
h = StencilHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if h > 0 {
|
||||||
if startX == -1 {
|
if startX == -1 {
|
||||||
startX = x
|
startX = x
|
||||||
|
currentHeight = h
|
||||||
|
} else if h != currentHeight {
|
||||||
|
// Height changed, end current strip and start new one
|
||||||
|
stripLen := x - startX
|
||||||
|
AddBox(
|
||||||
|
&triangles,
|
||||||
|
float64(startX)*PixelToMM,
|
||||||
|
float64(y)*PixelToMM,
|
||||||
|
float64(stripLen)*PixelToMM,
|
||||||
|
PixelToMM,
|
||||||
|
currentHeight,
|
||||||
|
)
|
||||||
|
startX = x
|
||||||
|
currentHeight = h
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if startX != -1 {
|
if startX != -1 {
|
||||||
|
|
@ -109,9 +407,10 @@ func GenerateMeshFromImage(img image.Image) [][3]Point {
|
||||||
float64(y)*PixelToMM,
|
float64(y)*PixelToMM,
|
||||||
float64(stripLen)*PixelToMM,
|
float64(stripLen)*PixelToMM,
|
||||||
PixelToMM,
|
PixelToMM,
|
||||||
StencilHeight,
|
currentHeight,
|
||||||
)
|
)
|
||||||
startX = -1
|
startX = -1
|
||||||
|
currentHeight = 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -123,7 +422,7 @@ func GenerateMeshFromImage(img image.Image) [][3]Point {
|
||||||
float64(y)*PixelToMM,
|
float64(y)*PixelToMM,
|
||||||
float64(stripLen)*PixelToMM,
|
float64(stripLen)*PixelToMM,
|
||||||
PixelToMM,
|
PixelToMM,
|
||||||
StencilHeight,
|
currentHeight,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -134,33 +433,83 @@ func GenerateMeshFromImage(img image.Image) [][3]Point {
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Float64Var(&StencilHeight, "height", 0.2, "Stencil height in mm")
|
flag.Float64Var(&StencilHeight, "height", 0.2, "Stencil height in mm")
|
||||||
flag.Float64Var(&StencilHeight, "h", 0.2, "Stencil height in mm (short)")
|
flag.Float64Var(&WallHeight, "wall-height", 2.0, "Wall height in mm")
|
||||||
|
flag.Float64Var(&WallThickness, "wall-thickness", 1, "Wall thickness in mm")
|
||||||
|
flag.Float64Var(&DPI, "dpi", 1000.0, "DPI for rendering (lower = smaller file, rougher curves)")
|
||||||
flag.BoolVar(&KeepPNG, "keep-png", false, "Save intermediate PNG file")
|
flag.BoolVar(&KeepPNG, "keep-png", false, "Save intermediate PNG file")
|
||||||
flag.BoolVar(&KeepPNG, "kp", false, "Save intermediate PNG file (short)")
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
// Update PixelToMM based on DPI flag
|
||||||
|
PixelToMM = 25.4 / DPI
|
||||||
|
|
||||||
args := flag.Args()
|
args := flag.Args()
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
fmt.Println("Usage: go run main.go [options] <path_to_gerber_file>")
|
fmt.Println("Usage: go run main.go [options] <path_to_gerber_file> [path_to_outline_gerber_file]")
|
||||||
fmt.Println("Options:")
|
fmt.Println("Options:")
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
fmt.Println("Example: go run main.go -height=0.3 MyPCB.GTP")
|
fmt.Println("Example: go run main.go -height=0.3 MyPCB.GTP MyPCB.GKO")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
gerberPath := args[0]
|
gerberPath := args[0]
|
||||||
|
var outlinePath string
|
||||||
|
if len(args) > 1 {
|
||||||
|
outlinePath = args[1]
|
||||||
|
}
|
||||||
|
|
||||||
outputPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + ".stl"
|
outputPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + ".stl"
|
||||||
|
|
||||||
// 1. Parse Gerber
|
// 1. Parse Gerber(s)
|
||||||
fmt.Printf("Parsing %s...\n", gerberPath)
|
fmt.Printf("Parsing %s...\n", gerberPath)
|
||||||
gf, err := ParseGerber(gerberPath)
|
gf, err := ParseGerber(gerberPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error parsing gerber: %v", err)
|
log.Fatalf("Error parsing gerber: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Render to Image
|
var outlineGf *GerberFile
|
||||||
|
if outlinePath != "" {
|
||||||
|
fmt.Printf("Parsing outline %s...\n", outlinePath)
|
||||||
|
outlineGf, err = ParseGerber(outlinePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error parsing outline gerber: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Calculate Union Bounds
|
||||||
|
bounds := gf.CalculateBounds()
|
||||||
|
if outlineGf != nil {
|
||||||
|
outlineBounds := outlineGf.CalculateBounds()
|
||||||
|
if outlineBounds.MinX < bounds.MinX {
|
||||||
|
bounds.MinX = outlineBounds.MinX
|
||||||
|
}
|
||||||
|
if outlineBounds.MinY < bounds.MinY {
|
||||||
|
bounds.MinY = outlineBounds.MinY
|
||||||
|
}
|
||||||
|
if outlineBounds.MaxX > bounds.MaxX {
|
||||||
|
bounds.MaxX = outlineBounds.MaxX
|
||||||
|
}
|
||||||
|
if outlineBounds.MaxY > bounds.MaxY {
|
||||||
|
bounds.MaxY = outlineBounds.MaxY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand bounds to accommodate wall thickness and prevent clipping
|
||||||
|
// We add WallThickness + extra margin to all sides
|
||||||
|
margin := WallThickness + 5.0 // mm
|
||||||
|
bounds.MinX -= margin
|
||||||
|
bounds.MinY -= margin
|
||||||
|
bounds.MaxX += margin
|
||||||
|
bounds.MaxY += margin
|
||||||
|
|
||||||
|
// 3. Render to Image(s)
|
||||||
fmt.Println("Rendering to internal image...")
|
fmt.Println("Rendering to internal image...")
|
||||||
img := gf.Render(DPI)
|
img := gf.Render(DPI, &bounds)
|
||||||
|
|
||||||
|
var outlineImg image.Image
|
||||||
|
if outlineGf != nil {
|
||||||
|
fmt.Println("Rendering outline to internal image...")
|
||||||
|
outlineImg = outlineGf.Render(DPI, &bounds)
|
||||||
|
}
|
||||||
|
|
||||||
if KeepPNG {
|
if KeepPNG {
|
||||||
pngPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + ".png"
|
pngPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + ".png"
|
||||||
|
|
@ -174,13 +523,27 @@ func main() {
|
||||||
}
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if outlineImg != nil {
|
||||||
|
outlinePngPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + "_outline.png"
|
||||||
|
fmt.Printf("Saving intermediate Outline PNG to %s...\n", outlinePngPath)
|
||||||
|
f, err := os.Create(outlinePngPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Could not create Outline PNG file: %v", err)
|
||||||
|
} else {
|
||||||
|
if err := png.Encode(f, outlineImg); err != nil {
|
||||||
|
log.Printf("Warning: Could not encode Outline PNG: %v", err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Generate Mesh
|
// 4. Generate Mesh
|
||||||
fmt.Println("Generating mesh (this may take 10-20 seconds for large boards)...")
|
fmt.Println("Generating mesh (this may take 10-20 seconds for large boards)...")
|
||||||
triangles := GenerateMeshFromImage(img)
|
triangles := GenerateMeshFromImages(img, outlineImg)
|
||||||
|
|
||||||
// 4. Save STL
|
// 5. Save STL
|
||||||
fmt.Printf("Saving to %s (%d triangles)...\n", outputPath, len(triangles))
|
fmt.Printf("Saving to %s (%d triangles)...\n", outputPath, len(triangles))
|
||||||
err = WriteSTL(outputPath, triangles)
|
err = WriteSTL(outputPath, triangles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue