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 }