373 lines
11 KiB
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
|
|
}
|