Added webserver option
This commit is contained in:
parent
c4c33ee120
commit
c1b626b006
|
|
@ -0,0 +1 @@
|
||||||
|
./temp/*
|
||||||
18
README.md
18
README.md
|
|
@ -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
342
main.go
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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*
|
||||||
|
|
@ -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*
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue