pcb-to-stencil/scad.go

373 lines
11 KiB
Go

package main
import (
"fmt"
"math"
"os"
)
func WriteSCAD(filename string, triangles [][3]Point) error {
// Fallback/legacy mesh WriteSCAD
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
fmt.Fprintf(f, "// Generated by pcb-to-stencil\npolyhedron(\n points=[\n")
for i, t := range triangles {
fmt.Fprintf(f, " [%f, %f, %f], [%f, %f, %f], [%f, %f, %f]", t[0].X, t[0].Y, t[0].Z, t[1].X, t[1].Y, t[1].Z, t[2].X, t[2].Y, t[2].Z)
if i < len(triangles)-1 {
fmt.Fprintf(f, ",\n")
} else {
fmt.Fprintf(f, "\n")
}
}
fmt.Fprintf(f, " ],\n faces=[\n")
for i := 0; i < len(triangles); i++ {
idx := i * 3
fmt.Fprintf(f, " [%d, %d, %d]", idx, idx+1, idx+2)
if i < len(triangles)-1 {
fmt.Fprintf(f, ",\n")
} else {
fmt.Fprintf(f, "\n")
}
}
fmt.Fprintf(f, " ]\n);\n")
return nil
}
// ExtractPolygonFromGerber traces the Edge.Cuts gerber to form a continuous 2D polygon
func ExtractPolygonFromGerber(gf *GerberFile) [][2]float64 {
var strokes [][][2]float64
var currentStroke [][2]float64
curX, curY := 0.0, 0.0
interpolationMode := "G01"
for _, cmd := range gf.Commands {
if cmd.Type == "G01" || cmd.Type == "G02" || cmd.Type == "G03" {
interpolationMode = cmd.Type
continue
}
prevX, prevY := curX, curY
if cmd.X != nil {
curX = *cmd.X
}
if cmd.Y != nil {
curY = *cmd.Y
}
if cmd.Type == "MOVE" {
if len(currentStroke) > 0 {
strokes = append(strokes, currentStroke)
currentStroke = nil
}
} else if cmd.Type == "DRAW" {
if len(currentStroke) == 0 {
currentStroke = append(currentStroke, [2]float64{prevX, prevY})
}
if interpolationMode == "G01" {
currentStroke = append(currentStroke, [2]float64{curX, curY})
} else {
iVal, jVal := 0.0, 0.0
if cmd.I != nil {
iVal = *cmd.I
}
if cmd.J != nil {
jVal = *cmd.J
}
centerX, centerY := prevX+iVal, prevY+jVal
radius := math.Sqrt(iVal*iVal + jVal*jVal)
startAngle := math.Atan2(prevY-centerY, prevX-centerX)
endAngle := math.Atan2(curY-centerY, curX-centerX)
if interpolationMode == "G03" {
if endAngle <= startAngle {
endAngle += 2 * math.Pi
}
} else {
if startAngle <= endAngle {
startAngle += 2 * math.Pi
}
}
arcLen := math.Abs(endAngle-startAngle) * radius
steps := int(arcLen * 10)
if steps < 5 {
steps = 5
}
for s := 1; s <= steps; s++ {
t := float64(s) / float64(steps)
a := startAngle + t*(endAngle-startAngle)
ax, ay := centerX+radius*math.Cos(a), centerY+radius*math.Sin(a)
currentStroke = append(currentStroke, [2]float64{ax, ay})
}
}
}
}
if len(currentStroke) > 0 {
strokes = append(strokes, currentStroke)
}
if len(strokes) == 0 {
return nil
}
// Stitch strokes into closed loops
var loops [][][2]float64
used := make([]bool, len(strokes))
epsilon := 0.05 // 0.05mm tolerance
for startIdx := 0; startIdx < len(strokes); startIdx++ {
if used[startIdx] {
continue
}
used[startIdx] = true
path := append([][2]float64{}, strokes[startIdx]...)
for {
endPt := path[len(path)-1]
startPt := path[0]
found := false
for j := 0; j < len(strokes); j++ {
if used[j] {
continue
}
s := strokes[j]
sStart := s[0]
sEnd := s[len(s)-1]
dist := func(a, b [2]float64) float64 {
dx, dy := a[0]-b[0], a[1]-b[1]
return math.Sqrt(dx*dx + dy*dy)
}
if dist(endPt, sStart) < epsilon {
path = append(path, s[1:]...)
used[j] = true
found = true
break
} else if dist(endPt, sEnd) < epsilon {
for k := len(s) - 2; k >= 0; k-- {
path = append(path, s[k])
}
used[j] = true
found = true
break
} else if dist(startPt, sEnd) < epsilon {
// prepend
newPath := append([][2]float64{}, s[:len(s)-1]...)
path = append(newPath, path...)
used[j] = true
found = true
break
} else if dist(startPt, sStart) < epsilon {
// reversed prepend
var newPath [][2]float64
for k := len(s) - 1; k > 0; k-- {
newPath = append(newPath, s[k])
}
path = append(newPath, path...)
used[j] = true
found = true
break
}
}
if !found {
break
}
}
loops = append(loops, path)
}
// Find the longest loop (the main board outline)
var bestLoop [][2]float64
maxLen := 0.0
for _, l := range loops {
loopLen := 0.0
for i := 0; i < len(l)-1; i++ {
dx := l[i+1][0] - l[i][0]
dy := l[i+1][1] - l[i][1]
loopLen += math.Sqrt(dx*dx + dy*dy)
}
if loopLen > maxLen {
maxLen = loopLen
bestLoop = l
}
}
// Always ensure path is closed
if len(bestLoop) > 2 {
first := bestLoop[0]
last := bestLoop[len(bestLoop)-1]
if math.Abs(first[0]-last[0]) > epsilon || math.Abs(first[1]-last[1]) > epsilon {
bestLoop = append(bestLoop, first)
}
}
return bestLoop
}
// WriteNativeSCAD generates native parametrically defined CSG OpenSCAD code
func WriteNativeSCAD(filename string, isTray bool, outlineVertices [][2]float64, cfg EnclosureConfig, holes []DrillHole, cutouts []SideCutout, sides []BoardSide, minBX, maxBX, boardCenterY float64) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
fmt.Fprintf(f, "// Generated by pcb-to-stencil (Native SCAD)\n")
fmt.Fprintf(f, "$fn = 60;\n\n")
// 1. Output the Board Polygon Module
fmt.Fprintf(f, "module board_polygon() {\n polygon(points=[\n")
for i, v := range outlineVertices {
fmt.Fprintf(f, " [%f, %f]", v[0], v[1])
if i < len(outlineVertices)-1 {
fmt.Fprintf(f, ",\n")
}
}
fmt.Fprintf(f, "\n ]);\n}\n\n")
// Dimensions
clearance := cfg.Clearance
wt := cfg.WallThickness
lidThick := wt
snapHeight := 2.5
trayFloor := 1.5
pcbT := cfg.PCBThickness
totalH := cfg.WallHeight + pcbT + trayFloor
lipH := pcbT + 1.5
// Create Peg and Socket helper
fmt.Fprintf(f, "module mounting_pegs(isSocket) {\n")
for _, h := range holes {
if h.Type == DrillTypeMounting {
r := (h.Diameter / 2.0) - 0.15
if isTray {
// We subtract sockets from the tray floor
r = (h.Diameter / 2.0) + 0.1
fmt.Fprintf(f, " translate([%f, %f, -1]) cylinder(r=%f, h=%f);\n", h.X, h.Y, r, trayFloor+2)
} else {
fmt.Fprintf(f, " translate([%f, %f, 0]) cylinder(r=%f, h=%f);\n", h.X, h.Y, r, totalH-lidThick)
}
}
}
fmt.Fprintf(f, "}\n\n")
// Print Side Cutouts module
fmt.Fprintf(f, "module side_cutouts() {\n")
for _, c := range cutouts {
var bs *BoardSide
for i := range sides {
if sides[i].Num == c.Side {
bs = &sides[i]
break
}
}
if bs == nil {
continue
}
// Cutouts are relative to board.
z := c.Height/2 + trayFloor + pcbT
w, d, h := c.Width, 20.0, c.Height // d is deep enough to cut through walls
dx := bs.EndX - bs.StartX
dy := bs.EndY - bs.StartY
length := math.Sqrt(dx*dx + dy*dy)
if length > 0 {
dx /= length
dy /= length
}
midX := bs.StartX + dx*(c.X+w/2)
midY := bs.StartY + dy*(c.X+w/2)
rotDeg := (bs.Angle * 180.0 / math.Pi) - 90.0
fmt.Fprintf(f, " translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n", midX, midY, z, rotDeg, w, d, h)
}
fmt.Fprintf(f, "}\n\n")
// Print Pry Slots Module
fmt.Fprintf(f, "module pry_slots() {\n")
pryW := 8.0
pryD := 1.5
fmt.Fprintf(f, " translate([%f, %f, 0]) cube([%f, %f, %f], center=true);\n", minBX-clearance-wt+pryD/2, boardCenterY, pryD*2, pryW, snapHeight*3)
fmt.Fprintf(f, " translate([%f, %f, 0]) cube([%f, %f, %f], center=true);\n", maxBX+clearance+wt-pryD/2, boardCenterY, pryD*2, pryW, snapHeight*3)
fmt.Fprintf(f, "}\n\n")
if isTray {
// --- TRAY ---
fmt.Fprintf(f, "// --- TRAY ---\n")
fmt.Fprintf(f, "difference() {\n")
fmt.Fprintf(f, " union() {\n")
fmt.Fprintf(f, " // Tray Floor (extends to clearance + 2*wt so it is flush with enclosure outside)\n")
fmt.Fprintf(f, " linear_extrude(height=%f) offset(r=%f) board_polygon();\n", trayFloor, clearance+2*wt)
fmt.Fprintf(f, " // Tray Inner Wall (thickness wt)\n")
fmt.Fprintf(f, " translate([0,0,%f]) linear_extrude(height=%f) difference() {\n", trayFloor, snapHeight)
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance+wt)
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance)
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, " // Snap Bumps (on outside of tray wall)\n")
fmt.Fprintf(f, " translate([0,0,%f]) linear_extrude(height=%f) difference() {\n", trayFloor+snapHeight-0.6, 0.4)
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance+wt+0.4)
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance+wt)
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, " // Mounting Pegs\n")
for _, hole := range holes {
if hole.Type != DrillTypeMounting {
continue
}
pegRadius := (hole.Diameter / 2.0) - 0.15
fmt.Fprintf(f, " translate([%f,%f,0]) cylinder(h=%f, r=%f, $fn=32);\n", hole.X, hole.Y, totalH-lidThick, pegRadius)
}
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, " // Subtract Lip Recess (for easy opening)\n")
fmt.Fprintf(f, " translate([0,0,-1]) linear_extrude(height=%f) difference() {\n", trayFloor+lipH+0.5)
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance+2*wt+1.0)
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance+2*wt-0.5) // Assuming lipCut is 0.5mm
fmt.Fprintf(f, " }\n")
// Remove peg holes from floor
for _, hole := range holes {
if hole.Type != DrillTypeMounting {
continue
}
socketRadius := (hole.Diameter / 2.0) + 0.1
fmt.Fprintf(f, " translate([%f,%f,-1]) cylinder(h=%f, r=%f, $fn=32);\n", hole.X, hole.Y, trayFloor+2, socketRadius)
}
fmt.Fprintf(f, " pry_slots();\n")
fmt.Fprintf(f, " side_cutouts();\n")
fmt.Fprintf(f, "}\n\n")
} else {
// --- ENCLOSURE ---
fmt.Fprintf(f, "// --- ENCLOSURE ---\n")
fmt.Fprintf(f, "difference() {\n")
fmt.Fprintf(f, " union() {\n")
fmt.Fprintf(f, " // Outer Enclosure block (accommodates Tray Wall + Enclosure Wall)\n")
fmt.Fprintf(f, " translate([0,0,%f]) linear_extrude(height=%f) offset(r=%f) board_polygon();\n", trayFloor, totalH-trayFloor, clearance+2*wt)
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, " // Subtract Inner Cavity (Base clearance around board)\n")
fmt.Fprintf(f, " translate([0,0,-1]) linear_extrude(height=%f) offset(r=%f) board_polygon();\n", totalH-lidThick+1, clearance)
fmt.Fprintf(f, " // Subtract Tray Recess (Accommodates Tray Wall)\n")
fmt.Fprintf(f, " translate([0,0,-1]) linear_extrude(height=%f) offset(r=%f) board_polygon();\n", trayFloor+snapHeight+0.2, clearance+wt+0.15)
fmt.Fprintf(f, " // Subtract Snap Groove\n")
fmt.Fprintf(f, " translate([0,0,%f]) linear_extrude(height=%f) difference() {\n", trayFloor+snapHeight-0.7, 0.6)
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance+wt+0.5)
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance+wt)
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, " pry_slots();\n")
fmt.Fprintf(f, " side_cutouts();\n")
fmt.Fprintf(f, "}\n")
fmt.Fprintf(f, "mounting_pegs(false);\n")
}
return nil
}