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 points [][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 == "DRAW" { if len(points) == 0 { points = append(points, [2]float64{prevX, prevY}) } if interpolationMode == "G01" { points = append(points, [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) points = append(points, [2]float64{ax, ay}) } } } } return points } // WriteNativeSCAD generates native parametrically defined CSG OpenSCAD code func WriteNativeSCAD(filename string, isTray bool, outlineVertices [][2]float64, cfg EnclosureConfig, holes []DrillHole, cutouts []SideCutout, 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 { // Cutouts are relative to board. x, y, z := 0.0, 0.0, c.Height/2+trayFloor+pcbT w, d, h := c.Width, 20.0, c.Height // d is deep enough to cut through walls if c.Side == 0 { // Top y = outlineVertices[0][1] + 10 // rough outside pos x = c.X fmt.Fprintf(f, " translate([%f, %f, %f]) cube([%f, %f, %f], center=true);\n", x, y, z, w, d, h) } else if c.Side == 1 { // Right x = maxBX y = c.Y fmt.Fprintf(f, " translate([%f, %f, %f]) cube([%f, %f, %f], center=true);\n", x, y, z, d, w, h) } else if c.Side == 2 { // Bottom y = outlineVertices[0][1] - 10 x = c.X fmt.Fprintf(f, " translate([%f, %f, %f]) cube([%f, %f, %f], center=true);\n", x, y, z, w, d, h) } else if c.Side == 3 { // Left x = minBX y = c.Y fmt.Fprintf(f, " translate([%f, %f, %f]) cube([%f, %f, %f], center=true);\n", x, y, z, d, w, 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 }