Added webserver option

This commit is contained in:
Nikolai Danylchyk 2025-12-14 11:48:34 +01:00
parent c4c33ee120
commit c1b626b006
9 changed files with 1254 additions and 79 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
./temp/*

View File

@ -15,12 +15,12 @@ A Go tool to convert Gerber files (specifically solder paste layers) into 3D pri
Run the tool using `go run`: Run the tool using `go run`:
```bash ```bash
go run main.go gerber.go [options] <path_to_gerber_file> go run main.go gerber.go [options] <path_to_gerber_file> [optional_board_outline_file]
``` ```
### Options ### Options
- `--height`: Stencil height in mm (default: 0.2mm). - `--height`: Stencil height in mm (default: 0.16mm).
- `--wall-height`: Wall height mm (default: 2.0mm). - `--wall-height`: Wall height mm (default: 2.0mm).
- `--wall-thickness`: Wall thickness in mm (default: 1mm). - `--wall-thickness`: Wall thickness in mm (default: 1mm).
- `--keep-png`: Save the intermediate PNG image used for mesh generation (useful for debugging). - `--keep-png`: Save the intermediate PNG image used for mesh generation (useful for debugging).
@ -28,11 +28,23 @@ go run main.go gerber.go [options] <path_to_gerber_file>
### Example ### Example
```bash ```bash
go run main.go gerber.go -height=0.25 -keep-png my_board_paste_top.gbr go run main.go gerber.go -height=0.16 -keep-png my_board_paste_top.gbr my_board_outline.gbr
``` ```
This will generate `my_board_paste_top.stl` in the same directory. This will generate `my_board_paste_top.stl` in the same directory.
## 3D Printing Recommendations
For optimal results with small SMD packages (like TSSOP, 0402, etc.), use the following 3D print settings:
- **Nozzle Size**: 0.2mm (Highly recommended for sharp corners and fine apertures).
- **Layer Height**: 0.16mm total height.
- **First Layer**: 0.10mm
- **Second Layer**: 0.06mm
- **Build Surface**: Smooth PEI sheet (Ensures the bottom of the stencil is perfectly flat for good PCB adhesion).
These settings assume you run the tool with `-height=0.16` (the default).
## How it Works ## How it Works
1. **Parsing**: The tool reads the Gerber file and interprets the drawing commands (flashes and draws). 1. **Parsing**: The tool reads the Gerber file and interprets the drawing commands (flashes and draws).

342
main.go
View File

@ -1,26 +1,42 @@
package main package main
import ( import (
"crypto/rand"
"embed"
"encoding/binary" "encoding/binary"
"encoding/hex"
"flag" "flag"
"fmt" "fmt"
"html/template"
"image" "image"
"image/png" "image/png"
"io"
"log" "log"
"math" "math"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
) )
// --- Configuration --- // --- Configuration ---
var DPI float64 = 1000.0 // Higher DPI = smoother curves
var PixelToMM float64 = 25.4 / DPI
var StencilHeight float64 = 0.16 // mm, default type Config struct {
var WallHeight float64 = 2.0 // mm, default StencilHeight float64
var WallThickness float64 = 1.0 // mm, default WallHeight float64
var KeepPNG bool WallThickness float64
DPI float64
KeepPNG bool
}
// Default values
const (
DefaultStencilHeight = 0.16
DefaultWallHeight = 2.0
DefaultWallThickness = 1.0
DefaultDPI = 1000.0
)
// --- STL Helpers --- // --- STL Helpers ---
@ -122,7 +138,7 @@ func AddBox(triangles *[][3]Point, x, y, w, h, zHeight float64) {
// ComputeWallMask generates a mask for the wall based on the outline image. // 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 // It identifies the board area (inside the outline) and creates a wall of
// specified thickness around it. // specified thickness around it.
func ComputeWallMask(img image.Image, thicknessMM float64) ([]bool, []bool) { func ComputeWallMask(img image.Image, thicknessMM float64, pixelToMM float64) ([]bool, []bool) {
bounds := img.Bounds() bounds := img.Bounds()
w := bounds.Max.X w := bounds.Max.X
h := bounds.Max.Y h := bounds.Max.Y
@ -149,7 +165,7 @@ func ComputeWallMask(img image.Image, thicknessMM float64) ([]bool, []bool) {
// 2. Dilate Outline to close gaps // 2. Dilate Outline to close gaps
// We dilate by a small amount (e.g. 0.5mm) to ensure the outline is closed. // We dilate by a small amount (e.g. 0.5mm) to ensure the outline is closed.
gapClosingMM := 0.5 gapClosingMM := 0.5
gapClosingPixels := int(gapClosingMM / PixelToMM) gapClosingPixels := int(gapClosingMM / pixelToMM)
if gapClosingPixels < 1 { if gapClosingPixels < 1 {
gapClosingPixels = 1 gapClosingPixels = 1
} }
@ -283,7 +299,7 @@ func ComputeWallMask(img image.Image, thicknessMM float64) ([]bool, []bool) {
// We want the wall to be strictly OUTSIDE the board (or centered on outline? User said "starts at outline"). // 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. // If we expand Board, we get pixels outside.
thicknessPixels := int(thicknessMM / PixelToMM) thicknessPixels := int(thicknessMM / pixelToMM)
if thicknessPixels < 1 { if thicknessPixels < 1 {
thicknessPixels = 1 thicknessPixels = 1
} }
@ -334,7 +350,8 @@ func ComputeWallMask(img image.Image, thicknessMM float64) ([]bool, []bool) {
return isWall, isBoard return isWall, isBoard
} }
func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point { func GenerateMeshFromImages(stencilImg, outlineImg image.Image, cfg Config) [][3]Point {
pixelToMM := 25.4 / cfg.DPI
bounds := stencilImg.Bounds() bounds := stencilImg.Bounds()
width := bounds.Max.X width := bounds.Max.X
height := bounds.Max.Y height := bounds.Max.Y
@ -344,7 +361,7 @@ func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point {
var boardMask []bool var boardMask []bool
if outlineImg != nil { if outlineImg != nil {
fmt.Println("Computing wall mask...") fmt.Println("Computing wall mask...")
wallMask, boardMask = ComputeWallMask(outlineImg, WallThickness) wallMask, boardMask = ComputeWallMask(outlineImg, cfg.WallThickness, pixelToMM)
} }
// Optimization: Run-Length Encoding // Optimization: Run-Length Encoding
@ -372,10 +389,10 @@ func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point {
// Determine height at this pixel // Determine height at this pixel
h := 0.0 h := 0.0
if isWall { if isWall {
h = WallHeight h = cfg.WallHeight
} else if isStencilSolid { } else if isStencilSolid {
if isInsideBoard { if isInsideBoard {
h = StencilHeight h = cfg.StencilHeight
} }
} }
@ -388,10 +405,10 @@ func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point {
stripLen := x - startX stripLen := x - startX
AddBox( AddBox(
&triangles, &triangles,
float64(startX)*PixelToMM, float64(startX)*pixelToMM,
float64(y)*PixelToMM, float64(y)*pixelToMM,
float64(stripLen)*PixelToMM, float64(stripLen)*pixelToMM,
PixelToMM, pixelToMM,
currentHeight, currentHeight,
) )
startX = x startX = x
@ -403,10 +420,10 @@ func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point {
stripLen := x - startX stripLen := x - startX
AddBox( AddBox(
&triangles, &triangles,
float64(startX)*PixelToMM, float64(startX)*pixelToMM,
float64(y)*PixelToMM, float64(y)*pixelToMM,
float64(stripLen)*PixelToMM, float64(stripLen)*pixelToMM,
PixelToMM, pixelToMM,
currentHeight, currentHeight,
) )
startX = -1 startX = -1
@ -418,10 +435,10 @@ func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point {
stripLen := width - startX stripLen := width - startX
AddBox( AddBox(
&triangles, &triangles,
float64(startX)*PixelToMM, float64(startX)*pixelToMM,
float64(y)*PixelToMM, float64(y)*pixelToMM,
float64(stripLen)*PixelToMM, float64(stripLen)*pixelToMM,
PixelToMM, pixelToMM,
currentHeight, currentHeight,
) )
} }
@ -429,41 +446,16 @@ func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point {
return triangles return triangles
} }
// --- Main --- // --- Logic ---
func main() {
flag.Float64Var(&StencilHeight, "height", 0.16, "Stencil height in mm")
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.Parse()
// Update PixelToMM based on DPI flag
PixelToMM = 25.4 / DPI
args := flag.Args()
if len(args) < 1 {
fmt.Println("Usage: go run main.go [options] <path_to_gerber_file> [path_to_outline_gerber_file]")
fmt.Println("Options:")
flag.PrintDefaults()
fmt.Println("Example: go run main.go -height=0.3 MyPCB.GTP MyPCB.GKO")
os.Exit(1)
}
gerberPath := args[0]
var outlinePath string
if len(args) > 1 {
outlinePath = args[1]
}
func processPCB(gerberPath, outlinePath string, cfg Config) (string, error) {
outputPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + ".stl" outputPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + ".stl"
// 1. Parse Gerber(s) // 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) return "", fmt.Errorf("error parsing gerber: %v", err)
} }
var outlineGf *GerberFile var outlineGf *GerberFile
@ -471,7 +463,7 @@ func main() {
fmt.Printf("Parsing outline %s...\n", outlinePath) fmt.Printf("Parsing outline %s...\n", outlinePath)
outlineGf, err = ParseGerber(outlinePath) outlineGf, err = ParseGerber(outlinePath)
if err != nil { if err != nil {
log.Fatalf("Error parsing outline gerber: %v", err) return "", fmt.Errorf("error parsing outline gerber: %v", err)
} }
} }
@ -494,8 +486,7 @@ func main() {
} }
// Expand bounds to accommodate wall thickness and prevent clipping // Expand bounds to accommodate wall thickness and prevent clipping
// We add WallThickness + extra margin to all sides margin := cfg.WallThickness + 5.0 // mm
margin := WallThickness + 5.0 // mm
bounds.MinX -= margin bounds.MinX -= margin
bounds.MinY -= margin bounds.MinY -= margin
bounds.MaxX += margin bounds.MaxX += margin
@ -503,15 +494,15 @@ func main() {
// 3. Render to Image(s) // 3. Render to Image(s)
fmt.Println("Rendering to internal image...") fmt.Println("Rendering to internal image...")
img := gf.Render(DPI, &bounds) img := gf.Render(cfg.DPI, &bounds)
var outlineImg image.Image var outlineImg image.Image
if outlineGf != nil { if outlineGf != nil {
fmt.Println("Rendering outline to internal image...") fmt.Println("Rendering outline to internal image...")
outlineImg = outlineGf.Render(DPI, &bounds) outlineImg = outlineGf.Render(cfg.DPI, &bounds)
} }
if KeepPNG { if cfg.KeepPNG {
pngPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + ".png" pngPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + ".png"
fmt.Printf("Saving intermediate PNG to %s...\n", pngPath) fmt.Printf("Saving intermediate PNG to %s...\n", pngPath)
f, err := os.Create(pngPath) f, err := os.Create(pngPath)
@ -523,32 +514,231 @@ 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()
}
}
} }
// 4. Generate Mesh // 4. Generate Mesh
fmt.Println("Generating mesh (this may take 10-20 seconds for large boards)...") fmt.Println("Generating mesh...")
triangles := GenerateMeshFromImages(img, outlineImg) triangles := GenerateMeshFromImages(img, outlineImg, cfg)
// 5. 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 {
log.Fatalf("Error writing STL: %v", err) return "", fmt.Errorf("error writing STL: %v", err)
} }
return outputPath, nil
}
// --- CLI ---
func runCLI(cfg Config, args []string) {
if len(args) < 1 {
fmt.Println("Usage: go run main.go [options] <path_to_gerber_file> [path_to_outline_gerber_file]")
fmt.Println("Options:")
flag.PrintDefaults()
fmt.Println("Example: go run main.go -height=0.3 MyPCB.GTP MyPCB.GKO")
os.Exit(1)
}
gerberPath := args[0]
var outlinePath string
if len(args) > 1 {
outlinePath = args[1]
}
_, err := processPCB(gerberPath, outlinePath, cfg)
if err != nil {
log.Fatalf("Error: %v", err)
}
fmt.Println("Success! Happy printing.") fmt.Println("Success! Happy printing.")
} }
// --- Server ---
//go:embed static/*
var staticFiles embed.FS
func randomID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
// Read index.html from embedded FS
content, err := staticFiles.ReadFile("static/index.html")
if err != nil {
http.Error(w, "Could not load index page", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(content)
}
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Create temp dir
tempDir := filepath.Join(".", "temp")
os.MkdirAll(tempDir, 0755)
uuid := randomID()
// Parse params
height, _ := strconv.ParseFloat(r.FormValue("height"), 64)
dpi, _ := strconv.ParseFloat(r.FormValue("dpi"), 64)
wallHeight, _ := strconv.ParseFloat(r.FormValue("wallHeight"), 64)
wallThickness, _ := strconv.ParseFloat(r.FormValue("wallThickness"), 64)
if height == 0 {
height = DefaultStencilHeight
}
if dpi == 0 {
dpi = DefaultDPI
}
if wallHeight == 0 {
wallHeight = DefaultWallHeight
}
if wallThickness == 0 {
wallThickness = DefaultWallThickness
}
cfg := Config{
StencilHeight: height,
WallHeight: wallHeight,
WallThickness: wallThickness,
DPI: dpi,
KeepPNG: false,
}
// Handle Gerber File
file, header, err := r.FormFile("gerber")
if err != nil {
http.Error(w, "Error retrieving gerber file", http.StatusBadRequest)
return
}
defer file.Close()
gerberPath := filepath.Join(tempDir, uuid+"_paste"+filepath.Ext(header.Filename))
outFile, err := os.Create(gerberPath)
if err != nil {
http.Error(w, "Server error saving file", http.StatusInternalServerError)
return
}
defer outFile.Close()
io.Copy(outFile, file)
// Handle Outline File (Optional)
outlineFile, outlineHeader, err := r.FormFile("outline")
var outlinePath string
if err == nil {
defer outlineFile.Close()
outlinePath = filepath.Join(tempDir, uuid+"_outline"+filepath.Ext(outlineHeader.Filename))
outOutline, err := os.Create(outlinePath)
if err == nil {
defer outOutline.Close()
io.Copy(outOutline, outlineFile)
}
}
// Process
outSTL, err := processPCB(gerberPath, outlinePath, cfg)
if err != nil {
log.Printf("Error processing: %v", err)
http.Error(w, fmt.Sprintf("Error processing PCB: %v", err), http.StatusInternalServerError)
return
}
// Render Success
tmpl, err := template.ParseFS(staticFiles, "static/result.html")
if err != nil {
http.Error(w, "Template error", http.StatusInternalServerError)
return
}
data := struct{ Filename string }{Filename: filepath.Base(outSTL)}
tmpl.Execute(w, data)
}
func downloadHandler(w http.ResponseWriter, r *http.Request) {
vars := strings.Split(r.URL.Path, "/")
if len(vars) < 3 {
http.NotFound(w, r)
return
}
filename := vars[2]
// Security check: ensure no path traversal
if strings.Contains(filename, "/") || strings.Contains(filename, "\\") || strings.Contains(filename, "..") {
http.Error(w, "Invalid filename", http.StatusBadRequest)
return
}
path := filepath.Join("temp", filename)
if _, err := os.Stat(path); os.IsNotExist(err) {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
w.Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(w, r, path)
}
func runServer(port string) {
// Serve static files (CSS, etc.)
// This will serve files under /static/ from the embedded fs
http.Handle("/static/", http.FileServer(http.FS(staticFiles)))
http.HandleFunc("/", indexHandler)
http.HandleFunc("/upload", uploadHandler)
http.HandleFunc("/download/", downloadHandler)
fmt.Printf("Starting server on http://0.0.0.0:%s\n", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
// --- Main ---
var (
flagStencilHeight float64
flagWallHeight float64
flagWallThickness float64
flagDPI float64
flagKeepPNG bool
flagServer bool
flagPort string
)
func main() {
flag.Float64Var(&flagStencilHeight, "height", DefaultStencilHeight, "Stencil height in mm")
flag.Float64Var(&flagWallHeight, "wall-height", DefaultWallHeight, "Wall height in mm")
flag.Float64Var(&flagWallThickness, "wall-thickness", DefaultWallThickness, "Wall thickness in mm")
flag.Float64Var(&flagDPI, "dpi", DefaultDPI, "DPI for rendering (lower = smaller file, rougher curves)")
flag.BoolVar(&flagKeepPNG, "keep-png", false, "Save intermediate PNG file")
flag.BoolVar(&flagServer, "server", false, "Start in server mode")
flag.StringVar(&flagPort, "port", "8080", "Port to run the server on")
flag.Parse()
if flagServer {
runServer(flagPort)
} else {
cfg := Config{
StencilHeight: flagStencilHeight,
WallHeight: flagWallHeight,
WallThickness: flagWallThickness,
DPI: flagDPI,
KeepPNG: flagKeepPNG,
}
runCLI(cfg, flag.Args())
}
}

728
paste.gbr Normal file
View File

@ -0,0 +1,728 @@
%TF.GenerationSoftware,Altium Limited,Altium Designer,25.8.1 (18)*%
G04 Layer_Color=8421504*
%FSLAX45Y45*%
%MOMM*%
%TF.SameCoordinates,86462A3C-2A55-4F76-B35D-D1350583A7ED*%
%TF.FilePolarity,Positive*%
%TF.FileFunction,Paste,Top*%
%TF.Part,Single*%
G01*
G75*
%TA.AperFunction,SMDPad,CuDef*%
G04:AMPARAMS|DCode=10|XSize=1.65543mm|YSize=0.38077mm|CornerRadius=0.19038mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=0.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD10*
21,1,1.65543,0.00000,0,0,0.0*
21,1,1.27467,0.38077,0,0,0.0*
1,1,0.38077,0.63733,0.00000*
1,1,0.38077,-0.63733,0.00000*
1,1,0.38077,-0.63733,0.00000*
1,1,0.38077,0.63733,0.00000*
%
%ADD10ROUNDEDRECTD10*%
%ADD11R,1.65543X0.38077*%
%ADD12R,0.91213X0.85872*%
%ADD13R,0.95814X0.91213*%
%ADD14R,0.91213X0.95814*%
%ADD15R,0.90000X1.50000*%
G04:AMPARAMS|DCode=16|XSize=1.45mm|YSize=0.3mm|CornerRadius=0.0495mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=180.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD16*
21,1,1.45000,0.20100,0,0,180.0*
21,1,1.35100,0.30000,0,0,180.0*
1,1,0.09900,-0.67550,0.10050*
1,1,0.09900,0.67550,0.10050*
1,1,0.09900,0.67550,-0.10050*
1,1,0.09900,-0.67550,-0.10050*
%
%ADD16ROUNDEDRECTD16*%
%ADD17R,0.85872X0.91213*%
%ADD18R,1.50000X0.90000*%
%TA.AperFunction,BGAPad,CuDef*%
%ADD19R,0.90000X0.90000*%
%TA.AperFunction,SMDPad,CuDef*%
G04:AMPARAMS|DCode=20|XSize=0.4mm|YSize=1.2mm|CornerRadius=0.05mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=270.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD20*
21,1,0.40000,1.10000,0,0,270.0*
21,1,0.30000,1.20000,0,0,270.0*
1,1,0.10000,-0.55000,-0.15000*
1,1,0.10000,-0.55000,0.15000*
1,1,0.10000,0.55000,0.15000*
1,1,0.10000,0.55000,-0.15000*
%
%ADD20ROUNDEDRECTD20*%
%ADD21R,0.24247X0.83971*%
%ADD22R,1.00000X1.40000*%
%ADD23R,0.30000X1.15000*%
G04:AMPARAMS|DCode=25|XSize=1.35712mm|YSize=0.57213mm|CornerRadius=0.28606mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=0.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD25*
21,1,1.35712,0.00000,0,0,0.0*
21,1,0.78499,0.57213,0,0,0.0*
1,1,0.57213,0.39250,0.00000*
1,1,0.57213,-0.39250,0.00000*
1,1,0.57213,-0.39250,0.00000*
1,1,0.57213,0.39250,0.00000*
%
%ADD25ROUNDEDRECTD25*%
G04:AMPARAMS|DCode=26|XSize=0.24247mm|YSize=0.83971mm|CornerRadius=0.12124mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=270.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD26*
21,1,0.24247,0.59723,0,0,270.0*
21,1,0.00000,0.83971,0,0,270.0*
1,1,0.24247,-0.29862,0.00000*
1,1,0.24247,-0.29862,0.00000*
1,1,0.24247,0.29862,0.00000*
1,1,0.24247,0.29862,0.00000*
%
%ADD26ROUNDEDRECTD26*%
%ADD27R,1.35712X0.57213*%
G04:AMPARAMS|DCode=28|XSize=1.65543mm|YSize=0.38077mm|CornerRadius=0.19038mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=270.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD28*
21,1,1.65543,0.00000,0,0,270.0*
21,1,1.27467,0.38077,0,0,270.0*
1,1,0.38077,0.00000,-0.63733*
1,1,0.38077,0.00000,0.63733*
1,1,0.38077,0.00000,0.63733*
1,1,0.38077,0.00000,-0.63733*
%
%ADD28ROUNDEDRECTD28*%
%TA.AperFunction,ConnectorPad*%
%ADD29R,1.15000X0.30000*%
%ADD30R,1.15000X0.60000*%
%TA.AperFunction,SMDPad,CuDef*%
G04:AMPARAMS|DCode=32|XSize=0.83971mm|YSize=0.24247mm|CornerRadius=0.12124mm|HoleSize=0mm|Usage=FLASHONLY|Rotation=270.000|XOffset=0mm|YOffset=0mm|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD32*
21,1,0.83971,0.00000,0,0,270.0*
21,1,0.59723,0.24247,0,0,270.0*
1,1,0.24247,0.00000,-0.29862*
1,1,0.24247,0.00000,0.29862*
1,1,0.24247,0.00000,0.29862*
1,1,0.24247,0.00000,-0.29862*
%
%ADD32ROUNDEDRECTD32*%
%ADD33R,1.20000X2.20000*%
%ADD34R,0.80000X1.45000*%
%ADD35R,0.70000X0.60000*%
%ADD36R,3.50000X2.20000*%
%ADD38R,0.60000X1.15000*%
%ADD39R,0.38077X1.65543*%
%TA.AperFunction,NonConductor*%
%ADD113R,1.29652X1.29652*%
%TA.AperFunction,SMDPad,CuDef*%
%ADD114R,0.30000X0.60000*%
%ADD115R,0.60000X0.30000*%
D10*
X4613238Y11757080D02*
D03*
Y11692080D02*
D03*
Y11627080D02*
D03*
Y11562080D02*
D03*
Y11497080D02*
D03*
Y11432080D02*
D03*
Y11367080D02*
D03*
X4048162D02*
D03*
Y11432080D02*
D03*
Y11497080D02*
D03*
Y11562080D02*
D03*
Y11627080D02*
D03*
Y11692080D02*
D03*
D11*
Y11757080D02*
D03*
D12*
X698500Y10376972D02*
D03*
X2733040Y10095032D02*
D03*
X2471420D02*
D03*
X3459480Y9282633D02*
D03*
X4097020D02*
D03*
X4732020Y9280093D02*
D03*
X2733040Y10250373D02*
D03*
X2471420D02*
D03*
X988060Y12787429D02*
D03*
X769620D02*
D03*
X698500Y10532313D02*
D03*
X8137083Y10095433D02*
D03*
Y9940092D02*
D03*
X7719060Y10442387D02*
D03*
Y10287046D02*
D03*
X7183120Y14052351D02*
D03*
X5981700D02*
D03*
X4780280D02*
D03*
X3583940D02*
D03*
X7332980Y9048552D02*
D03*
X988060Y12942770D02*
D03*
X3583940Y14207693D02*
D03*
X7183120D02*
D03*
X7332980Y9203893D02*
D03*
X4780280Y14207693D02*
D03*
X5981700D02*
D03*
X769620Y12942770D02*
D03*
X3459480Y9127292D02*
D03*
X4097020D02*
D03*
X4732020Y9124752D02*
D03*
D13*
X3582980Y9852660D02*
D03*
X4626920Y11968480D02*
D03*
X4481520D02*
D03*
X3605220Y12349480D02*
D03*
X5756600Y8923020D02*
D03*
X7875580Y8851900D02*
D03*
X7730180D02*
D03*
X6668460Y9403080D02*
D03*
X3750620Y12349480D02*
D03*
X3437580Y9852660D02*
D03*
X5902000Y8923020D02*
D03*
X6813860Y9403080D02*
D03*
D14*
X259080Y10524797D02*
D03*
X485140Y10527340D02*
D03*
X911860Y10524800D02*
D03*
X6192520Y12836200D02*
D03*
X6977380Y12554260D02*
D03*
X7122160Y9198920D02*
D03*
X6913880Y9198919D02*
D03*
X6700520Y9201460D02*
D03*
X6977380Y12408860D02*
D03*
X7495540D02*
D03*
X7239000D02*
D03*
X5989320Y13605820D02*
D03*
X5783580D02*
D03*
X6192520Y12690800D02*
D03*
X7386320Y10273340D02*
D03*
X5577840Y13605820D02*
D03*
X5534660Y12121841D02*
D03*
X6913880Y9053520D02*
D03*
X7122160Y9053520D02*
D03*
X6700520Y9056060D02*
D03*
X911860Y10379400D02*
D03*
X485140Y10381940D02*
D03*
X259080Y10379403D02*
D03*
X5534660Y12267239D02*
D03*
X7239000Y12554260D02*
D03*
X7495540D02*
D03*
X7386320Y10127940D02*
D03*
X5989320Y13460422D02*
D03*
X5783580D02*
D03*
X5577840D02*
D03*
D15*
X256403Y10838200D02*
D03*
X383403D02*
D03*
X510403D02*
D03*
X637403D02*
D03*
X764403D02*
D03*
X891403D02*
D03*
X1018403D02*
D03*
X1145403D02*
D03*
X1272403D02*
D03*
X1399403D02*
D03*
X1526403D02*
D03*
X1653403D02*
D03*
X1780403D02*
D03*
X1653403Y12588199D02*
D03*
X1526403D02*
D03*
X1399403D02*
D03*
X1272403D02*
D03*
X1145403D02*
D03*
X1018403D02*
D03*
X891403D02*
D03*
X764403D02*
D03*
X510403D02*
D03*
X383403D02*
D03*
X256403D02*
D03*
X637403D02*
D03*
X1780403D02*
D03*
X129403Y10838200D02*
D03*
Y12588199D02*
D03*
D16*
X3910040Y9850120D02*
D03*
Y9750120D02*
D03*
Y9800123D02*
D03*
X4350040Y9850120D02*
D03*
X3910040Y9900122D02*
D03*
Y9950120D02*
D03*
X4350040D02*
D03*
Y9900122D02*
D03*
Y9800123D02*
D03*
Y9750120D02*
D03*
D17*
X7735768Y11348720D02*
D03*
X1479352Y13406120D02*
D03*
X7727752Y9291320D02*
D03*
X7730289Y9509760D02*
D03*
X7885631D02*
D03*
X7883093Y9291320D02*
D03*
X7885633Y9723120D02*
D03*
X7883093Y9070340D02*
D03*
X7336993Y9385300D02*
D03*
X7181652D02*
D03*
X7580427Y11348720D02*
D03*
X7730292Y9723120D02*
D03*
X7727752Y9070340D02*
D03*
X1934007Y9403639D02*
D03*
X1505148D02*
D03*
X1634693Y13406120D02*
D03*
X2089348Y9403639D02*
D03*
X1349807D02*
D03*
D18*
X1905401Y11014700D02*
D03*
Y11141700D02*
D03*
Y11268700D02*
D03*
Y11395700D02*
D03*
Y11522700D02*
D03*
Y11649700D02*
D03*
Y11776700D02*
D03*
Y11903700D02*
D03*
Y12030700D02*
D03*
Y12157700D02*
D03*
Y12284700D02*
D03*
Y12411700D02*
D03*
D19*
X761401Y11423198D02*
D03*
X901400D02*
D03*
X761401Y11703202D02*
D03*
Y11563198D02*
D03*
X901400Y11703202D02*
D03*
Y11563198D02*
D03*
X1041400D02*
D03*
Y11423198D02*
D03*
Y11703202D02*
D03*
D20*
X5432542Y9563039D02*
D03*
Y9498040D02*
D03*
X3287278Y11894881D02*
D03*
X2717282Y12024878D02*
D03*
Y12284878D02*
D03*
X3287278Y11764879D02*
D03*
X2717282Y11894881D02*
D03*
Y12154880D02*
D03*
X3287278Y12349881D02*
D03*
X5432542Y9173042D02*
D03*
X2717282Y12219879D02*
D03*
Y12089882D02*
D03*
Y11959880D02*
D03*
Y11829882D02*
D03*
X3287278Y12219879D02*
D03*
Y12154880D02*
D03*
Y12089882D02*
D03*
Y12024878D02*
D03*
Y11959880D02*
D03*
Y11829882D02*
D03*
X2717282Y12349881D02*
D03*
X3287278Y12284878D02*
D03*
X2717282Y11764879D02*
D03*
X6002538Y9628043D02*
D03*
Y9563039D02*
D03*
Y9498040D02*
D03*
X5432542Y9628043D02*
D03*
Y9433042D02*
D03*
Y9368038D02*
D03*
Y9303040D02*
D03*
Y9238041D02*
D03*
X6002538Y9433042D02*
D03*
Y9368038D02*
D03*
Y9303040D02*
D03*
Y9238041D02*
D03*
Y9173042D02*
D03*
D21*
X7250100Y9952772D02*
D03*
D22*
X1166896Y10365740D02*
D03*
X1866900D02*
D03*
Y13055600D02*
D03*
X2164598Y11013440D02*
D03*
X2864602D02*
D03*
X1166896Y13055600D02*
D03*
D23*
X1621917Y8965763D02*
D03*
X1721917D02*
D03*
X1671919D02*
D03*
X1771919D02*
D03*
X1871919D02*
D03*
X1821917D02*
D03*
X1571920D02*
D03*
X1521917D02*
D03*
D25*
X7881620Y10005060D02*
D03*
X8163560Y10347960D02*
D03*
X7973446Y10252959D02*
D03*
X7691506Y9910059D02*
D03*
X912935Y13582401D02*
D03*
X722825Y13487399D02*
D03*
D26*
X7341652Y9711223D02*
D03*
X6958548D02*
D03*
Y9761220D02*
D03*
X7341652Y9811222D02*
D03*
Y9761220D02*
D03*
X6958548Y9861220D02*
D03*
Y9811222D02*
D03*
X7341652Y9661220D02*
D03*
Y9861220D02*
D03*
X6958548Y9661220D02*
D03*
D27*
X7973446Y10442961D02*
D03*
X7691506Y10100061D02*
D03*
X912935Y13392400D02*
D03*
D28*
X5859201Y12513981D02*
D03*
X5794202D02*
D03*
X5664200D02*
D03*
X5469199Y13079059D02*
D03*
X5534198Y12513981D02*
D03*
X5729199D02*
D03*
X5599201D02*
D03*
X5469199D02*
D03*
X5534198Y13079059D02*
D03*
X5599201D02*
D03*
X5664200D02*
D03*
X5729199D02*
D03*
X5794202D02*
D03*
D29*
X8374781Y9529679D02*
D03*
Y9579681D02*
D03*
Y9429679D02*
D03*
Y9479681D02*
D03*
Y9329679D02*
D03*
Y9379682D02*
D03*
Y9629678D02*
D03*
Y9679681D02*
D03*
D30*
Y9744680D02*
D03*
Y9264680D02*
D03*
Y9184681D02*
D03*
Y9824679D02*
D03*
D32*
X7100098Y9569668D02*
D03*
X7150100Y9952772D02*
D03*
Y9569668D02*
D03*
X7200097Y9952772D02*
D03*
X7100098D02*
D03*
X7050100D02*
D03*
X7200097Y9569668D02*
D03*
X7250100D02*
D03*
X7050100D02*
D03*
D33*
X6780400Y12905620D02*
D03*
X7010400D02*
D03*
X7240400D02*
D03*
D34*
X2489200Y12895499D02*
D03*
X2743200Y13520502D02*
D03*
X2489200D02*
D03*
X2743200Y12895499D02*
D03*
D35*
X7579360Y11119358D02*
D03*
Y11024362D02*
D03*
D36*
X7010400Y13525620D02*
D03*
D38*
X2016917Y8965763D02*
D03*
X1376919D02*
D03*
X1456919D02*
D03*
X1936918D02*
D03*
D39*
X5859201Y13079059D02*
D03*
D113*
X7150100Y9761220D02*
D03*
D114*
X1619402Y9208059D02*
D03*
X1773479D02*
D03*
X1843481D02*
D03*
X1549400D02*
D03*
D115*
X8181340Y9578900D02*
D03*
Y9430462D02*
D03*
X8110220Y8978341D02*
D03*
Y8908339D02*
D03*
X8181340Y9360460D02*
D03*
Y9648902D02*
D03*
%TF.MD5,8ce37c590d9a8131b045448d67d8b995*%
M02*

BIN
paste.stl Normal file

Binary file not shown.

35
profile.gbr Normal file
View File

@ -0,0 +1,35 @@
%TF.GenerationSoftware,Altium Limited,Altium Designer,25.8.1 (18)*%
G04 Layer_Color=0*
%FSLAX45Y45*%
%MOMM*%
%TF.SameCoordinates,86462A3C-2A55-4F76-B35D-D1350583A7ED*%
%TF.FilePolarity,Positive*%
%TF.FileFunction,Profile,NP*%
%TF.Part,Single*%
G01*
G75*
%TA.AperFunction,Profile*%
%ADD116C,0.02540*%
D116*
X25400Y8483600D02*
G03*
X152400Y8356600I127000J0D01*
G01*
X8864600D01*
D02*
G03*
X8991600Y8483600I0J127000D01*
G01*
Y14859000D01*
D02*
G03*
X8864600Y14986000I-127000J0D01*
G01*
X152400D01*
D02*
G03*
X25400Y14859000I0J-127000D01*
G01*
Y8483600D01*
%TF.MD5,059b59ffb9f66ccce6837941a6fe851c*%
M02*

62
static/index.html Normal file
View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gerber to Stencil converter</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<h1>PCB to Stencil Converter by kennycoder</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="gerber">Solder Paste Gerber File (Required)</label>
<input type="file" id="gerber" name="gerber" accept=".gbr,.gtp,.gbp" required>
</div>
<div class="form-group">
<label for="outline">Board Outline Gerber (Optional)</label>
<input type="file" id="outline" name="outline" accept=".gbr,.gko,.gm1">
<div class="hint">Upload this to automatically crop and generate walls.</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div class="form-group">
<label for="height">Stencil Height (mm)</label>
<input type="number" id="height" name="height" value="0.16" step="0.01">
</div>
<div class="form-group">
<label for="dpi">DPI</label>
<input type="number" id="dpi" name="dpi" value="1000" step="100">
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div class="form-group">
<label for="wallHeight">Wall Height (mm)</label>
<input type="number" id="wallHeight" name="wallHeight" value="2.0" step="0.1">
</div>
<div class="form-group">
<label for="wallThickness">Wall Thickness (mm)</label>
<input type="number" id="wallThickness" name="wallThickness" value="1.0" step="0.1">
</div>
</div>
<button type="submit" id="submit-btn">Convert to STL</button>
</form>
<div id="loading">
<div class="spinner"></div>
<div>Processing... This may take 10-20 seconds.</div>
</div>
</div>
<script>
document.querySelector('form').addEventListener('submit', function() {
document.getElementById('loading').style.display = 'block';
document.getElementById('submit-btn').disabled = true;
document.getElementById('submit-btn').innerText = 'Converting...';
});
</script>
</body>
</html>

17
static/result.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gerber to Stencil converter</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="card">
<h2>Success!</h2>
<p>Your stencil has been generated successfully.</p>
<a href="/download/{{.Filename}}" class="btn">Download STL</a>
<a href="/" class="btn secondary">Convert Another</a>
</div>
</body>
</html>

130
static/style.css Normal file
View File

@ -0,0 +1,130 @@
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--bg: #f3f4f6;
--card-bg: #ffffff;
--text: #1f2937;
--border: #e5e7eb;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--bg);
color: var(--text);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.container {
background-color: var(--card-bg);
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
width: 100%;
max-width: 500px;
}
.card {
background-color: var(--card-bg);
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
text-align: center;
max-width: 400px;
width: 100%;
}
h1 {
margin-top: 0;
margin-bottom: 1.5rem;
text-align: center;
font-size: 1.5rem;
color: var(--text);
}
h2 {
color: #059669;
margin-top: 0;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
font-size: 0.875rem;
}
input[type="text"],
input[type="number"],
input[type="file"] {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 6px;
box-sizing: border-box;
font-size: 1rem;
}
.hint {
font-size: 0.75rem;
color: #6b7280;
margin-top: 0.25rem;
}
button {
width: 100%;
background-color: var(--primary);
color: white;
padding: 0.75rem;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
margin-top: 1rem;
}
button:hover {
background-color: var(--primary-hover);
}
button:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
#loading {
display: none;
text-align: center;
margin-top: 1rem;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid var(--primary);
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto 0.5rem auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.btn {
display: inline-block;
background: var(--primary);
color: white;
padding: 0.75rem 1.5rem;
text-decoration: none;
border-radius: 6px;
margin-top: 1rem;
transition: 0.2s;
}
.btn:hover {
background: var(--primary-hover);
}
.secondary {
background: #e5e7eb;
color: #374151;
margin-left: 0.5rem;
}
.secondary:hover {
background: #d1d5db;
}