diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2077be6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +./temp/* \ No newline at end of file diff --git a/README.md b/README.md index 792f34b..900b345 100644 --- a/README.md +++ b/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`: ```bash -go run main.go gerber.go [options] +go run main.go gerber.go [options] [optional_board_outline_file] ``` ### 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-thickness`: Wall thickness in mm (default: 1mm). - `--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] ### Example ```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. +## 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 1. **Parsing**: The tool reads the Gerber file and interprets the drawing commands (flashes and draws). diff --git a/main.go b/main.go index 5795dfb..d1643c8 100644 --- a/main.go +++ b/main.go @@ -1,26 +1,42 @@ package main import ( + "crypto/rand" + "embed" "encoding/binary" + "encoding/hex" "flag" "fmt" + "html/template" "image" "image/png" + "io" "log" "math" + "net/http" "os" "path/filepath" + "strconv" "strings" ) // --- Configuration --- -var DPI float64 = 1000.0 // Higher DPI = smoother curves -var PixelToMM float64 = 25.4 / DPI -var StencilHeight float64 = 0.16 // mm, default -var WallHeight float64 = 2.0 // mm, default -var WallThickness float64 = 1.0 // mm, default -var KeepPNG bool +type Config struct { + StencilHeight float64 + WallHeight float64 + WallThickness float64 + DPI float64 + KeepPNG bool +} + +// Default values +const ( + DefaultStencilHeight = 0.16 + DefaultWallHeight = 2.0 + DefaultWallThickness = 1.0 + DefaultDPI = 1000.0 +) // --- 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. // 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) { +func ComputeWallMask(img image.Image, thicknessMM float64, pixelToMM float64) ([]bool, []bool) { bounds := img.Bounds() w := bounds.Max.X h := bounds.Max.Y @@ -149,7 +165,7 @@ func ComputeWallMask(img image.Image, thicknessMM float64) ([]bool, []bool) { // 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) + gapClosingPixels := int(gapClosingMM / pixelToMM) if 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"). // If we expand Board, we get pixels outside. - thicknessPixels := int(thicknessMM / PixelToMM) + thicknessPixels := int(thicknessMM / pixelToMM) if thicknessPixels < 1 { thicknessPixels = 1 } @@ -334,7 +350,8 @@ func ComputeWallMask(img image.Image, thicknessMM float64) ([]bool, []bool) { 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() width := bounds.Max.X height := bounds.Max.Y @@ -344,7 +361,7 @@ func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point { var boardMask []bool if outlineImg != nil { fmt.Println("Computing wall mask...") - wallMask, boardMask = ComputeWallMask(outlineImg, WallThickness) + wallMask, boardMask = ComputeWallMask(outlineImg, cfg.WallThickness, pixelToMM) } // Optimization: Run-Length Encoding @@ -372,10 +389,10 @@ func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point { // Determine height at this pixel h := 0.0 if isWall { - h = WallHeight + h = cfg.WallHeight } else if isStencilSolid { if isInsideBoard { - h = StencilHeight + h = cfg.StencilHeight } } @@ -388,10 +405,10 @@ func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point { stripLen := x - startX AddBox( &triangles, - float64(startX)*PixelToMM, - float64(y)*PixelToMM, - float64(stripLen)*PixelToMM, - PixelToMM, + float64(startX)*pixelToMM, + float64(y)*pixelToMM, + float64(stripLen)*pixelToMM, + pixelToMM, currentHeight, ) startX = x @@ -403,10 +420,10 @@ func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point { stripLen := x - startX AddBox( &triangles, - float64(startX)*PixelToMM, - float64(y)*PixelToMM, - float64(stripLen)*PixelToMM, - PixelToMM, + float64(startX)*pixelToMM, + float64(y)*pixelToMM, + float64(stripLen)*pixelToMM, + pixelToMM, currentHeight, ) startX = -1 @@ -418,10 +435,10 @@ func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point { stripLen := width - startX AddBox( &triangles, - float64(startX)*PixelToMM, - float64(y)*PixelToMM, - float64(stripLen)*PixelToMM, - PixelToMM, + float64(startX)*pixelToMM, + float64(y)*pixelToMM, + float64(stripLen)*pixelToMM, + pixelToMM, currentHeight, ) } @@ -429,41 +446,16 @@ func GenerateMeshFromImages(stencilImg, outlineImg image.Image) [][3]Point { return triangles } -// --- Main --- - -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_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] - } +// --- Logic --- +func processPCB(gerberPath, outlinePath string, cfg Config) (string, error) { outputPath := strings.TrimSuffix(gerberPath, filepath.Ext(gerberPath)) + ".stl" // 1. Parse Gerber(s) fmt.Printf("Parsing %s...\n", gerberPath) gf, err := ParseGerber(gerberPath) if err != nil { - log.Fatalf("Error parsing gerber: %v", err) + return "", fmt.Errorf("error parsing gerber: %v", err) } var outlineGf *GerberFile @@ -471,7 +463,7 @@ func main() { fmt.Printf("Parsing outline %s...\n", outlinePath) outlineGf, err = ParseGerber(outlinePath) 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 - // We add WallThickness + extra margin to all sides - margin := WallThickness + 5.0 // mm + margin := cfg.WallThickness + 5.0 // mm bounds.MinX -= margin bounds.MinY -= margin bounds.MaxX += margin @@ -503,15 +494,15 @@ func main() { // 3. Render to Image(s) fmt.Println("Rendering to internal image...") - img := gf.Render(DPI, &bounds) + img := gf.Render(cfg.DPI, &bounds) var outlineImg image.Image if outlineGf != nil { 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" fmt.Printf("Saving intermediate PNG to %s...\n", pngPath) f, err := os.Create(pngPath) @@ -523,32 +514,231 @@ func main() { } 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 - fmt.Println("Generating mesh (this may take 10-20 seconds for large boards)...") - triangles := GenerateMeshFromImages(img, outlineImg) + fmt.Println("Generating mesh...") + triangles := GenerateMeshFromImages(img, outlineImg, cfg) // 5. Save STL fmt.Printf("Saving to %s (%d triangles)...\n", outputPath, len(triangles)) err = WriteSTL(outputPath, triangles) if err != nil { - log.Fatalf("Error writing STL: %v", err) + 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_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.") } + +// --- 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()) + } +} diff --git a/paste.gbr b/paste.gbr new file mode 100644 index 0000000..55d0a54 --- /dev/null +++ b/paste.gbr @@ -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* diff --git a/paste.stl b/paste.stl new file mode 100644 index 0000000..e36e9cf Binary files /dev/null and b/paste.stl differ diff --git a/profile.gbr b/profile.gbr new file mode 100644 index 0000000..903f761 --- /dev/null +++ b/profile.gbr @@ -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* diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..508da2d --- /dev/null +++ b/static/index.html @@ -0,0 +1,62 @@ + + + + + + Gerber to Stencil converter + + + +
+

PCB to Stencil Converter by kennycoder

+
+
+ + +
+
+ + +
Upload this to automatically crop and generate walls.
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
Processing... This may take 10-20 seconds.
+
+
+ + + + \ No newline at end of file diff --git a/static/result.html b/static/result.html new file mode 100644 index 0000000..325c864 --- /dev/null +++ b/static/result.html @@ -0,0 +1,17 @@ + + + + + + Gerber to Stencil converter + + + +
+

Success!

+

Your stencil has been generated successfully.

+ Download STL + Convert Another +
+ + \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..1386404 --- /dev/null +++ b/static/style.css @@ -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; +} \ No newline at end of file