745 lines
18 KiB
Go
745 lines
18 KiB
Go
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 ---
|
|
|
|
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 ---
|
|
|
|
type Point struct {
|
|
X, Y, Z float64
|
|
}
|
|
|
|
func WriteSTL(filename string, triangles [][3]Point) error {
|
|
f, err := os.Create(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
// Write Binary STL Header (80 bytes)
|
|
header := make([]byte, 80)
|
|
copy(header, "Generated by pcb-to-stencil")
|
|
if _, err := f.Write(header); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write Number of Triangles (4 bytes uint32)
|
|
count := uint32(len(triangles))
|
|
if err := binary.Write(f, binary.LittleEndian, count); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write Triangles
|
|
// Each triangle is 50 bytes:
|
|
// Normal (3 floats = 12 bytes)
|
|
// Vertex 1 (3 floats = 12 bytes)
|
|
// Vertex 2 (3 floats = 12 bytes)
|
|
// Vertex 3 (3 floats = 12 bytes)
|
|
// Attribute byte count (2 bytes uint16)
|
|
|
|
// Buffer for a single triangle to minimize syscalls
|
|
buf := make([]byte, 50)
|
|
|
|
for _, t := range triangles {
|
|
// Normal (0,0,0)
|
|
binary.LittleEndian.PutUint32(buf[0:4], math.Float32bits(0))
|
|
binary.LittleEndian.PutUint32(buf[4:8], math.Float32bits(0))
|
|
binary.LittleEndian.PutUint32(buf[8:12], math.Float32bits(0))
|
|
|
|
// Vertex 1
|
|
binary.LittleEndian.PutUint32(buf[12:16], math.Float32bits(float32(t[0].X)))
|
|
binary.LittleEndian.PutUint32(buf[16:20], math.Float32bits(float32(t[0].Y)))
|
|
binary.LittleEndian.PutUint32(buf[20:24], math.Float32bits(float32(t[0].Z)))
|
|
|
|
// Vertex 2
|
|
binary.LittleEndian.PutUint32(buf[24:28], math.Float32bits(float32(t[1].X)))
|
|
binary.LittleEndian.PutUint32(buf[28:32], math.Float32bits(float32(t[1].Y)))
|
|
binary.LittleEndian.PutUint32(buf[32:36], math.Float32bits(float32(t[1].Z)))
|
|
|
|
// Vertex 3
|
|
binary.LittleEndian.PutUint32(buf[36:40], math.Float32bits(float32(t[2].X)))
|
|
binary.LittleEndian.PutUint32(buf[40:44], math.Float32bits(float32(t[2].Y)))
|
|
binary.LittleEndian.PutUint32(buf[44:48], math.Float32bits(float32(t[2].Z)))
|
|
|
|
// Attribute byte count (0)
|
|
binary.LittleEndian.PutUint16(buf[48:50], 0)
|
|
|
|
if _, err := f.Write(buf); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func AddBox(triangles *[][3]Point, x, y, w, h, zHeight float64) {
|
|
x0, y0 := x, y
|
|
x1, y1 := x+w, y+h
|
|
z0, z1 := 0.0, zHeight
|
|
|
|
p000 := Point{x0, y0, z0}
|
|
p100 := Point{x1, y0, z0}
|
|
p110 := Point{x1, y1, z0}
|
|
p010 := Point{x0, y1, z0}
|
|
p001 := Point{x0, y0, z1}
|
|
p101 := Point{x1, y0, z1}
|
|
p111 := Point{x1, y1, z1}
|
|
p011 := Point{x0, y1, z1}
|
|
|
|
addQuad := func(a, b, c, d Point) {
|
|
*triangles = append(*triangles, [3]Point{a, b, c})
|
|
*triangles = append(*triangles, [3]Point{c, d, a})
|
|
}
|
|
|
|
addQuad(p000, p010, p110, p100) // Bottom
|
|
addQuad(p101, p111, p011, p001) // Top
|
|
addQuad(p000, p100, p101, p001) // Front
|
|
addQuad(p100, p110, p111, p101) // Right
|
|
addQuad(p110, p010, p011, p111) // Back
|
|
addQuad(p010, p000, p001, p011) // Left
|
|
}
|
|
|
|
// --- Meshing Logic (Optimized) ---
|
|
|
|
// 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, pixelToMM float64) ([]bool, []bool) {
|
|
bounds := img.Bounds()
|
|
w := bounds.Max.X
|
|
h := bounds.Max.Y
|
|
size := w * h
|
|
|
|
// Helper for neighbors
|
|
dx := []int{0, 0, 1, -1}
|
|
dy := []int{1, -1, 0, 0}
|
|
|
|
// 1. Identify Outline Pixels (White)
|
|
isOutline := make([]bool, size)
|
|
outlineQueue := []int{}
|
|
for i := 0; i < size; i++ {
|
|
cx := i % w
|
|
cy := i / w
|
|
c := img.At(cx, cy)
|
|
r, _, _, _ := c.RGBA()
|
|
if r > 10000 { // White-ish
|
|
isOutline[i] = true
|
|
outlineQueue = append(outlineQueue, i)
|
|
}
|
|
}
|
|
|
|
// 2. Dilate Outline to close gaps
|
|
// We dilate by a small amount (e.g. 0.5mm) to ensure the outline is closed.
|
|
gapClosingMM := 0.5
|
|
gapClosingPixels := int(gapClosingMM / pixelToMM)
|
|
if gapClosingPixels < 1 {
|
|
gapClosingPixels = 1
|
|
}
|
|
|
|
dist := make([]int, size)
|
|
for i := 0; i < size; i++ {
|
|
if isOutline[i] {
|
|
dist[i] = 0
|
|
} else {
|
|
dist[i] = -1
|
|
}
|
|
}
|
|
|
|
// BFS for Dilation
|
|
dilatedOutline := make([]bool, size)
|
|
copy(dilatedOutline, isOutline)
|
|
|
|
// Use a separate queue for dilation to avoid modifying the original outlineQueue if we needed it
|
|
dQueue := make([]int, len(outlineQueue))
|
|
copy(dQueue, outlineQueue)
|
|
|
|
for len(dQueue) > 0 {
|
|
idx := dQueue[0]
|
|
dQueue = dQueue[1:]
|
|
|
|
d := dist[idx]
|
|
if d >= gapClosingPixels {
|
|
continue
|
|
}
|
|
|
|
cx := idx % w
|
|
cy := idx / w
|
|
|
|
for i := 0; i < 4; i++ {
|
|
nx, ny := cx+dx[i], cy+dy[i]
|
|
if nx >= 0 && nx < w && ny >= 0 && ny < h {
|
|
nIdx := ny*w + nx
|
|
if dist[nIdx] == -1 {
|
|
dist[nIdx] = d + 1
|
|
dilatedOutline[nIdx] = true
|
|
dQueue = append(dQueue, nIdx)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Flood Fill "Outside" using Dilated Outline as barrier
|
|
isOutside := make([]bool, size)
|
|
// Start from (0,0) - assumed to be outside due to padding
|
|
if !dilatedOutline[0] {
|
|
isOutside[0] = true
|
|
fQueue := []int{0}
|
|
|
|
for len(fQueue) > 0 {
|
|
idx := fQueue[0]
|
|
fQueue = fQueue[1:]
|
|
|
|
cx := idx % w
|
|
cy := idx / w
|
|
|
|
for i := 0; i < 4; i++ {
|
|
nx, ny := cx+dx[i], cy+dy[i]
|
|
if nx >= 0 && nx < w && ny >= 0 && ny < h {
|
|
nIdx := ny*w + nx
|
|
if !isOutside[nIdx] && !dilatedOutline[nIdx] {
|
|
isOutside[nIdx] = true
|
|
fQueue = append(fQueue, nIdx)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Restore Board Shape (Erode "Outside" back to original boundary)
|
|
// We dilated the outline, so "Outside" stopped 'gapClosingPixels' away from the real board edge.
|
|
// We need to expand "Outside" inwards by 'gapClosingPixels' to touch the real board edge.
|
|
// Then "Board" = !Outside.
|
|
|
|
// Reset dist for Outside expansion
|
|
for i := 0; i < size; i++ {
|
|
if isOutside[i] {
|
|
dist[i] = 0
|
|
} else {
|
|
dist[i] = -1
|
|
}
|
|
}
|
|
|
|
oQueue := []int{}
|
|
for i := 0; i < size; i++ {
|
|
if isOutside[i] {
|
|
oQueue = append(oQueue, i)
|
|
}
|
|
}
|
|
|
|
isOutsideExpanded := make([]bool, size)
|
|
copy(isOutsideExpanded, isOutside)
|
|
|
|
for len(oQueue) > 0 {
|
|
idx := oQueue[0]
|
|
oQueue = oQueue[1:]
|
|
|
|
d := dist[idx]
|
|
if d >= gapClosingPixels {
|
|
continue
|
|
}
|
|
|
|
cx := idx % w
|
|
cy := idx / w
|
|
|
|
for i := 0; i < 4; i++ {
|
|
nx, ny := cx+dx[i], cy+dy[i]
|
|
if nx >= 0 && nx < w && ny >= 0 && ny < h {
|
|
nIdx := ny*w + nx
|
|
if dist[nIdx] == -1 {
|
|
dist[nIdx] = d + 1
|
|
isOutsideExpanded[nIdx] = true
|
|
oQueue = append(oQueue, nIdx)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. Define Board
|
|
isBoard := make([]bool, size)
|
|
for i := 0; i < size; i++ {
|
|
isBoard[i] = !isOutsideExpanded[i]
|
|
}
|
|
|
|
// 6. Generate Wall
|
|
// Wall is generated by expanding Board outwards.
|
|
// We want the wall to be strictly OUTSIDE the board (or centered on outline? User said "starts at outline").
|
|
// If we expand Board, we get pixels outside.
|
|
|
|
thicknessPixels := int(thicknessMM / pixelToMM)
|
|
if thicknessPixels < 1 {
|
|
thicknessPixels = 1
|
|
}
|
|
|
|
// Reset dist for Wall generation
|
|
for i := 0; i < size; i++ {
|
|
if isBoard[i] {
|
|
dist[i] = 0
|
|
} else {
|
|
dist[i] = -1
|
|
}
|
|
}
|
|
|
|
wQueue := []int{}
|
|
for i := 0; i < size; i++ {
|
|
if isBoard[i] {
|
|
wQueue = append(wQueue, i)
|
|
}
|
|
}
|
|
|
|
isWall := make([]bool, size)
|
|
|
|
for len(wQueue) > 0 {
|
|
idx := wQueue[0]
|
|
wQueue = wQueue[1:]
|
|
|
|
d := dist[idx]
|
|
if d >= thicknessPixels {
|
|
continue
|
|
}
|
|
|
|
cx := idx % w
|
|
cy := idx / w
|
|
|
|
for i := 0; i < 4; i++ {
|
|
nx, ny := cx+dx[i], cy+dy[i]
|
|
if nx >= 0 && nx < w && ny >= 0 && ny < h {
|
|
nIdx := ny*w + nx
|
|
if dist[nIdx] == -1 {
|
|
dist[nIdx] = d + 1
|
|
isWall[nIdx] = true
|
|
wQueue = append(wQueue, nIdx)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return isWall, isBoard
|
|
}
|
|
|
|
func GenerateMeshFromImages(stencilImg, outlineImg image.Image, cfg Config) [][3]Point {
|
|
pixelToMM := 25.4 / cfg.DPI
|
|
bounds := stencilImg.Bounds()
|
|
width := bounds.Max.X
|
|
height := bounds.Max.Y
|
|
var triangles [][3]Point
|
|
|
|
var wallMask []bool
|
|
var boardMask []bool
|
|
if outlineImg != nil {
|
|
fmt.Println("Computing wall mask...")
|
|
wallMask, boardMask = ComputeWallMask(outlineImg, cfg.WallThickness, pixelToMM)
|
|
}
|
|
|
|
// Optimization: Run-Length Encoding
|
|
for y := 0; y < height; y++ {
|
|
var startX = -1
|
|
var currentHeight = 0.0
|
|
|
|
for x := 0; x < width; x++ {
|
|
// Check stencil (black = solid)
|
|
sc := stencilImg.At(x, y)
|
|
sr, sg, sb, _ := sc.RGBA()
|
|
isStencilSolid := sr < 10000 && sg < 10000 && sb < 10000
|
|
|
|
// Check wall
|
|
isWall := false
|
|
isInsideBoard := true
|
|
if wallMask != nil {
|
|
idx := y*width + x
|
|
isWall = wallMask[idx]
|
|
if boardMask != nil {
|
|
isInsideBoard = boardMask[idx]
|
|
}
|
|
}
|
|
|
|
// Determine height at this pixel
|
|
h := 0.0
|
|
if isWall {
|
|
h = cfg.WallHeight
|
|
} else if isStencilSolid {
|
|
if isInsideBoard {
|
|
h = cfg.StencilHeight
|
|
}
|
|
}
|
|
|
|
if h > 0 {
|
|
if startX == -1 {
|
|
startX = x
|
|
currentHeight = h
|
|
} else if h != currentHeight {
|
|
// Height changed, end current strip and start new one
|
|
stripLen := x - startX
|
|
AddBox(
|
|
&triangles,
|
|
float64(startX)*pixelToMM,
|
|
float64(y)*pixelToMM,
|
|
float64(stripLen)*pixelToMM,
|
|
pixelToMM,
|
|
currentHeight,
|
|
)
|
|
startX = x
|
|
currentHeight = h
|
|
}
|
|
} else {
|
|
if startX != -1 {
|
|
// End of strip, generate box
|
|
stripLen := x - startX
|
|
AddBox(
|
|
&triangles,
|
|
float64(startX)*pixelToMM,
|
|
float64(y)*pixelToMM,
|
|
float64(stripLen)*pixelToMM,
|
|
pixelToMM,
|
|
currentHeight,
|
|
)
|
|
startX = -1
|
|
currentHeight = 0.0
|
|
}
|
|
}
|
|
}
|
|
if startX != -1 {
|
|
stripLen := width - startX
|
|
AddBox(
|
|
&triangles,
|
|
float64(startX)*pixelToMM,
|
|
float64(y)*pixelToMM,
|
|
float64(stripLen)*pixelToMM,
|
|
pixelToMM,
|
|
currentHeight,
|
|
)
|
|
}
|
|
}
|
|
return triangles
|
|
}
|
|
|
|
// --- 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 {
|
|
return "", fmt.Errorf("error parsing gerber: %v", err)
|
|
}
|
|
|
|
var outlineGf *GerberFile
|
|
if outlinePath != "" {
|
|
fmt.Printf("Parsing outline %s...\n", outlinePath)
|
|
outlineGf, err = ParseGerber(outlinePath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error parsing outline gerber: %v", err)
|
|
}
|
|
}
|
|
|
|
// 2. Calculate Union Bounds
|
|
bounds := gf.CalculateBounds()
|
|
if outlineGf != nil {
|
|
outlineBounds := outlineGf.CalculateBounds()
|
|
if outlineBounds.MinX < bounds.MinX {
|
|
bounds.MinX = outlineBounds.MinX
|
|
}
|
|
if outlineBounds.MinY < bounds.MinY {
|
|
bounds.MinY = outlineBounds.MinY
|
|
}
|
|
if outlineBounds.MaxX > bounds.MaxX {
|
|
bounds.MaxX = outlineBounds.MaxX
|
|
}
|
|
if outlineBounds.MaxY > bounds.MaxY {
|
|
bounds.MaxY = outlineBounds.MaxY
|
|
}
|
|
}
|
|
|
|
// Expand bounds to accommodate wall thickness and prevent clipping
|
|
margin := cfg.WallThickness + 5.0 // mm
|
|
bounds.MinX -= margin
|
|
bounds.MinY -= margin
|
|
bounds.MaxX += margin
|
|
bounds.MaxY += margin
|
|
|
|
// 3. Render to Image(s)
|
|
fmt.Println("Rendering to internal image...")
|
|
img := gf.Render(cfg.DPI, &bounds)
|
|
|
|
var outlineImg image.Image
|
|
if outlineGf != nil {
|
|
fmt.Println("Rendering outline to internal image...")
|
|
outlineImg = outlineGf.Render(cfg.DPI, &bounds)
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
log.Printf("Warning: Could not create PNG file: %v", err)
|
|
} else {
|
|
if err := png.Encode(f, img); err != nil {
|
|
log.Printf("Warning: Could not encode PNG: %v", err)
|
|
}
|
|
f.Close()
|
|
}
|
|
}
|
|
|
|
// 4. Generate Mesh
|
|
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 {
|
|
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.")
|
|
}
|
|
|
|
// --- 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())
|
|
}
|
|
}
|