1070 lines
35 KiB
Go
1070 lines
35 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"os"
|
||
)
|
||
|
||
// snapToLine rounds a dimension to the nearest quarter-multiple of lineWidth.
|
||
// If lineWidth is 0, the value is returned unchanged.
|
||
func snapToLine(v, lineWidth float64) float64 {
|
||
if lineWidth <= 0 {
|
||
return v
|
||
}
|
||
unit := lineWidth / 4.0
|
||
return math.Round(v/unit) * unit
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
// approximateArc returns intermediate arc points from (x1,y1) to (x2,y2),
|
||
// excluding the start point, including the end point.
|
||
func approximateArc(x1, y1, x2, y2, iVal, jVal float64, mode string) [][2]float64 {
|
||
centerX := x1 + iVal
|
||
centerY := y1 + jVal
|
||
radius := math.Sqrt(iVal*iVal + jVal*jVal)
|
||
startAngle := math.Atan2(y1-centerY, x1-centerX)
|
||
endAngle := math.Atan2(y2-centerY, x2-centerX)
|
||
if mode == "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 * 8)
|
||
if steps < 4 {
|
||
steps = 4
|
||
}
|
||
if steps > 128 {
|
||
steps = 128
|
||
}
|
||
pts := make([][2]float64, steps)
|
||
for s := 0; s < steps; s++ {
|
||
t := float64(s+1) / float64(steps)
|
||
a := startAngle + t*(endAngle-startAngle)
|
||
pts[s] = [2]float64{centerX + radius*math.Cos(a), centerY + radius*math.Sin(a)}
|
||
}
|
||
return pts
|
||
}
|
||
|
||
// writeApertureFlash2D writes a 2D aperture shape centered at (x, y) into a SCAD file.
|
||
// gf is needed to resolve macro apertures. lw is the nozzle line width for snapping.
|
||
func writeApertureFlash2D(f *os.File, gf *GerberFile, ap Aperture, x, y, lw float64, indent string) {
|
||
switch ap.Type {
|
||
case "C":
|
||
if len(ap.Modifiers) > 0 {
|
||
r := snapToLine(ap.Modifiers[0]/2, lw)
|
||
fmt.Fprintf(f, "%stranslate([%f, %f]) circle(r=%f);\n", indent, x, y, r)
|
||
}
|
||
case "R":
|
||
if len(ap.Modifiers) >= 2 {
|
||
w, h := snapToLine(ap.Modifiers[0], lw), snapToLine(ap.Modifiers[1], lw)
|
||
fmt.Fprintf(f, "%stranslate([%f, %f]) square([%f, %f], center=true);\n", indent, x, y, w, h)
|
||
}
|
||
case "O":
|
||
if len(ap.Modifiers) >= 2 {
|
||
w, h := snapToLine(ap.Modifiers[0], lw), snapToLine(ap.Modifiers[1], lw)
|
||
r := math.Min(w, h) / 2
|
||
fmt.Fprintf(f, "%stranslate([%f, %f]) hull() {\n", indent, x, y)
|
||
if w >= h {
|
||
d := (w - h) / 2
|
||
fmt.Fprintf(f, "%s translate([%f, 0]) circle(r=%f);\n", indent, d, r)
|
||
fmt.Fprintf(f, "%s translate([%f, 0]) circle(r=%f);\n", indent, -d, r)
|
||
} else {
|
||
d := (h - w) / 2
|
||
fmt.Fprintf(f, "%s translate([0, %f]) circle(r=%f);\n", indent, d, r)
|
||
fmt.Fprintf(f, "%s translate([0, %f]) circle(r=%f);\n", indent, -d, r)
|
||
}
|
||
fmt.Fprintf(f, "%s}\n", indent)
|
||
}
|
||
case "P":
|
||
if len(ap.Modifiers) >= 2 {
|
||
dia, numV := ap.Modifiers[0], int(ap.Modifiers[1])
|
||
r := snapToLine(dia/2, lw)
|
||
rot := 0.0
|
||
if len(ap.Modifiers) >= 3 {
|
||
rot = ap.Modifiers[2]
|
||
}
|
||
fmt.Fprintf(f, "%stranslate([%f, %f]) rotate([0, 0, %f]) circle(r=%f, $fn=%d);\n",
|
||
indent, x, y, rot, r, numV)
|
||
}
|
||
default:
|
||
// Macro aperture – compute bounding box from primitives and emit a simple square.
|
||
if gf == nil {
|
||
return
|
||
}
|
||
macro, ok := gf.State.Macros[ap.Type]
|
||
if !ok {
|
||
return
|
||
}
|
||
minX, minY := math.Inf(1), math.Inf(1)
|
||
maxX, maxY := math.Inf(-1), math.Inf(-1)
|
||
trackPt := func(px, py, radius float64) {
|
||
if px-radius < minX { minX = px - radius }
|
||
if px+radius > maxX { maxX = px + radius }
|
||
if py-radius < minY { minY = py - radius }
|
||
if py+radius > maxY { maxY = py + radius }
|
||
}
|
||
for _, prim := range macro.Primitives {
|
||
switch prim.Code {
|
||
case 1: // Circle
|
||
if len(prim.Modifiers) >= 4 {
|
||
dia := evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers)
|
||
cx := evaluateMacroExpression(prim.Modifiers[2], ap.Modifiers)
|
||
cy := evaluateMacroExpression(prim.Modifiers[3], ap.Modifiers)
|
||
trackPt(cx, cy, dia/2)
|
||
}
|
||
case 4: // Outline polygon
|
||
if len(prim.Modifiers) >= 3 {
|
||
numV := int(evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers))
|
||
for i := 0; i < numV && 2+i*2+1 < len(prim.Modifiers); i++ {
|
||
vx := evaluateMacroExpression(prim.Modifiers[2+i*2], ap.Modifiers)
|
||
vy := evaluateMacroExpression(prim.Modifiers[2+i*2+1], ap.Modifiers)
|
||
trackPt(vx, vy, 0)
|
||
}
|
||
}
|
||
case 20: // Vector line
|
||
if len(prim.Modifiers) >= 7 {
|
||
w := evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers)
|
||
sx := evaluateMacroExpression(prim.Modifiers[2], ap.Modifiers)
|
||
sy := evaluateMacroExpression(prim.Modifiers[3], ap.Modifiers)
|
||
ex := evaluateMacroExpression(prim.Modifiers[4], ap.Modifiers)
|
||
ey := evaluateMacroExpression(prim.Modifiers[5], ap.Modifiers)
|
||
trackPt(sx, sy, w/2)
|
||
trackPt(ex, ey, w/2)
|
||
}
|
||
case 21: // Center line rect
|
||
if len(prim.Modifiers) >= 6 {
|
||
w := evaluateMacroExpression(prim.Modifiers[1], ap.Modifiers)
|
||
h := evaluateMacroExpression(prim.Modifiers[2], ap.Modifiers)
|
||
cx := evaluateMacroExpression(prim.Modifiers[3], ap.Modifiers)
|
||
cy := evaluateMacroExpression(prim.Modifiers[4], ap.Modifiers)
|
||
trackPt(cx, cy, math.Max(w, h)/2)
|
||
}
|
||
}
|
||
}
|
||
if !math.IsInf(minX, 1) {
|
||
w := snapToLine(maxX-minX, lw)
|
||
h := snapToLine(maxY-minY, lw)
|
||
cx := (minX + maxX) / 2
|
||
cy := (minY + maxY) / 2
|
||
fmt.Fprintf(f, "%stranslate([%f, %f]) square([%f, %f], center=true);\n",
|
||
indent, x+cx, y+cy, w, h)
|
||
}
|
||
}
|
||
}
|
||
|
||
// writeMacroPrimitive2D emits a single macro primitive as 2D SCAD geometry.
|
||
func writeMacroPrimitive2D(f *os.File, prim MacroPrimitive, params []float64, indent string) {
|
||
switch prim.Code {
|
||
case 1: // Circle: Exposure, Diameter, CenterX, CenterY
|
||
if len(prim.Modifiers) >= 4 {
|
||
exposure := evaluateMacroExpression(prim.Modifiers[0], params)
|
||
if exposure == 0 {
|
||
return
|
||
}
|
||
dia := evaluateMacroExpression(prim.Modifiers[1], params)
|
||
cx := evaluateMacroExpression(prim.Modifiers[2], params)
|
||
cy := evaluateMacroExpression(prim.Modifiers[3], params)
|
||
fmt.Fprintf(f, "%stranslate([%f, %f]) circle(r=%f);\n", indent, cx, cy, dia/2)
|
||
}
|
||
case 4: // Outline (Polygon): Exposure, NumVertices, X1,Y1,...,Xn,Yn, Rotation
|
||
if len(prim.Modifiers) >= 3 {
|
||
exposure := evaluateMacroExpression(prim.Modifiers[0], params)
|
||
if exposure == 0 {
|
||
return
|
||
}
|
||
numV := int(evaluateMacroExpression(prim.Modifiers[1], params))
|
||
if len(prim.Modifiers) < 2+numV*2+1 {
|
||
return
|
||
}
|
||
rot := evaluateMacroExpression(prim.Modifiers[2+numV*2], params)
|
||
fmt.Fprintf(f, "%srotate([0, 0, %f]) polygon(points=[\n", indent, rot)
|
||
for i := 0; i < numV; i++ {
|
||
vx := evaluateMacroExpression(prim.Modifiers[2+i*2], params)
|
||
vy := evaluateMacroExpression(prim.Modifiers[2+i*2+1], params)
|
||
comma := ","
|
||
if i == numV-1 {
|
||
comma = ""
|
||
}
|
||
fmt.Fprintf(f, "%s [%f, %f]%s\n", indent, vx, vy, comma)
|
||
}
|
||
fmt.Fprintf(f, "%s]);\n", indent)
|
||
}
|
||
case 5: // Regular Polygon: Exposure, NumVertices, CenterX, CenterY, Diameter, Rotation
|
||
if len(prim.Modifiers) >= 6 {
|
||
exposure := evaluateMacroExpression(prim.Modifiers[0], params)
|
||
if exposure == 0 {
|
||
return
|
||
}
|
||
numV := int(evaluateMacroExpression(prim.Modifiers[1], params))
|
||
cx := evaluateMacroExpression(prim.Modifiers[2], params)
|
||
cy := evaluateMacroExpression(prim.Modifiers[3], params)
|
||
dia := evaluateMacroExpression(prim.Modifiers[4], params)
|
||
rot := evaluateMacroExpression(prim.Modifiers[5], params)
|
||
fmt.Fprintf(f, "%stranslate([%f, %f]) rotate([0, 0, %f]) circle(r=%f, $fn=%d);\n",
|
||
indent, cx, cy, rot, dia/2, numV)
|
||
}
|
||
case 20: // Vector Line: Exposure, Width, StartX, StartY, EndX, EndY, Rotation
|
||
if len(prim.Modifiers) >= 7 {
|
||
exposure := evaluateMacroExpression(prim.Modifiers[0], params)
|
||
if exposure == 0 {
|
||
return
|
||
}
|
||
width := evaluateMacroExpression(prim.Modifiers[1], params)
|
||
sx := evaluateMacroExpression(prim.Modifiers[2], params)
|
||
sy := evaluateMacroExpression(prim.Modifiers[3], params)
|
||
ex := evaluateMacroExpression(prim.Modifiers[4], params)
|
||
ey := evaluateMacroExpression(prim.Modifiers[5], params)
|
||
rot := evaluateMacroExpression(prim.Modifiers[6], params)
|
||
// hull() of two squares at start/end for a rectangle with the given width
|
||
fmt.Fprintf(f, "%srotate([0, 0, %f]) hull() {\n", indent, rot)
|
||
fmt.Fprintf(f, "%s translate([%f, %f]) square([0.001, %f], center=true);\n", indent, sx, sy, width)
|
||
fmt.Fprintf(f, "%s translate([%f, %f]) square([0.001, %f], center=true);\n", indent, ex, ey, width)
|
||
fmt.Fprintf(f, "%s}\n", indent)
|
||
}
|
||
case 21: // Center Line (Rect): Exposure, Width, Height, CenterX, CenterY, Rotation
|
||
if len(prim.Modifiers) >= 6 {
|
||
exposure := evaluateMacroExpression(prim.Modifiers[0], params)
|
||
if exposure == 0 {
|
||
return
|
||
}
|
||
w := evaluateMacroExpression(prim.Modifiers[1], params)
|
||
h := evaluateMacroExpression(prim.Modifiers[2], params)
|
||
cx := evaluateMacroExpression(prim.Modifiers[3], params)
|
||
cy := evaluateMacroExpression(prim.Modifiers[4], params)
|
||
rot := evaluateMacroExpression(prim.Modifiers[5], params)
|
||
fmt.Fprintf(f, "%stranslate([%f, %f]) rotate([0, 0, %f]) square([%f, %f], center=true);\n",
|
||
indent, cx, cy, rot, w, h)
|
||
}
|
||
}
|
||
}
|
||
|
||
// writeApertureLinearDraw2D writes a 2D stroke between two points using hull() of the aperture.
|
||
func writeApertureLinearDraw2D(f *os.File, ap Aperture, x1, y1, x2, y2 float64, indent string) {
|
||
switch ap.Type {
|
||
case "C":
|
||
if len(ap.Modifiers) > 0 {
|
||
r := ap.Modifiers[0] / 2
|
||
fmt.Fprintf(f, "%shull() {\n", indent)
|
||
fmt.Fprintf(f, "%s translate([%f, %f]) circle(r=%f);\n", indent, x1, y1, r)
|
||
fmt.Fprintf(f, "%s translate([%f, %f]) circle(r=%f);\n", indent, x2, y2, r)
|
||
fmt.Fprintf(f, "%s}\n", indent)
|
||
}
|
||
case "R":
|
||
if len(ap.Modifiers) >= 2 {
|
||
w, h := ap.Modifiers[0], ap.Modifiers[1]
|
||
fmt.Fprintf(f, "%shull() {\n", indent)
|
||
fmt.Fprintf(f, "%s translate([%f, %f]) square([%f, %f], center=true);\n", indent, x1, y1, w, h)
|
||
fmt.Fprintf(f, "%s translate([%f, %f]) square([%f, %f], center=true);\n", indent, x2, y2, w, h)
|
||
fmt.Fprintf(f, "%s}\n", indent)
|
||
}
|
||
default:
|
||
if len(ap.Modifiers) > 0 {
|
||
r := ap.Modifiers[0] / 2
|
||
fmt.Fprintf(f, "%shull() {\n", indent)
|
||
fmt.Fprintf(f, "%s translate([%f, %f]) circle(r=%f);\n", indent, x1, y1, r)
|
||
fmt.Fprintf(f, "%s translate([%f, %f]) circle(r=%f);\n", indent, x2, y2, r)
|
||
fmt.Fprintf(f, "%s}\n", indent)
|
||
}
|
||
}
|
||
}
|
||
|
||
// writeGerberShapes2D writes a 2D SCAD union body representing all drawn shapes
|
||
// from the Gerber file. Call this inside a union() block.
|
||
func writeGerberShapes2D(f *os.File, gf *GerberFile, lw float64, indent string) {
|
||
curX, curY := 0.0, 0.0
|
||
curDCode := 0
|
||
interpolationMode := "G01"
|
||
inRegion := false
|
||
var regionPts [][2]float64
|
||
|
||
for _, cmd := range gf.Commands {
|
||
if cmd.Type == "APERTURE" {
|
||
if cmd.D != nil {
|
||
curDCode = *cmd.D
|
||
}
|
||
continue
|
||
}
|
||
if cmd.Type == "G01" || cmd.Type == "G02" || cmd.Type == "G03" {
|
||
interpolationMode = cmd.Type
|
||
continue
|
||
}
|
||
if cmd.Type == "G36" {
|
||
inRegion = true
|
||
regionPts = nil
|
||
continue
|
||
}
|
||
if cmd.Type == "G37" {
|
||
if len(regionPts) >= 3 {
|
||
fmt.Fprintf(f, "%spolygon(points=[\n", indent)
|
||
for i, pt := range regionPts {
|
||
fmt.Fprintf(f, "%s [%f, %f]", indent, pt[0], pt[1])
|
||
if i < len(regionPts)-1 {
|
||
fmt.Fprintf(f, ",")
|
||
}
|
||
fmt.Fprintf(f, "\n")
|
||
}
|
||
fmt.Fprintf(f, "%s]);\n", indent)
|
||
}
|
||
inRegion = false
|
||
regionPts = nil
|
||
continue
|
||
}
|
||
|
||
prevX, prevY := curX, curY
|
||
if cmd.X != nil {
|
||
curX = *cmd.X
|
||
}
|
||
if cmd.Y != nil {
|
||
curY = *cmd.Y
|
||
}
|
||
|
||
if inRegion {
|
||
switch cmd.Type {
|
||
case "MOVE":
|
||
regionPts = append(regionPts, [2]float64{curX, curY})
|
||
case "DRAW":
|
||
if interpolationMode == "G01" {
|
||
regionPts = append(regionPts, [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
|
||
}
|
||
arcPts := approximateArc(prevX, prevY, curX, curY, iVal, jVal, interpolationMode)
|
||
regionPts = append(regionPts, arcPts...)
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
|
||
ap, ok := gf.State.Apertures[curDCode]
|
||
if !ok {
|
||
continue
|
||
}
|
||
switch cmd.Type {
|
||
case "FLASH":
|
||
writeApertureFlash2D(f, gf, ap, curX, curY, lw, indent)
|
||
case "DRAW":
|
||
if interpolationMode == "G01" {
|
||
writeApertureLinearDraw2D(f, ap, prevX, prevY, curX, curY, indent)
|
||
} else {
|
||
iVal, jVal := 0.0, 0.0
|
||
if cmd.I != nil {
|
||
iVal = *cmd.I
|
||
}
|
||
if cmd.J != nil {
|
||
jVal = *cmd.J
|
||
}
|
||
arcPts := approximateArc(prevX, prevY, curX, curY, iVal, jVal, interpolationMode)
|
||
all := append([][2]float64{{prevX, prevY}}, arcPts...)
|
||
for i := 0; i < len(all)-1; i++ {
|
||
writeApertureLinearDraw2D(f, ap, all[i][0], all[i][1], all[i+1][0], all[i+1][1], indent)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// WriteStencilSCAD generates native parametric OpenSCAD for a solder paste stencil.
|
||
// Instead of a rasterised mesh, it uses CSG primitives (circles, squares, hulls,
|
||
// polygons) so the result prints cleanly at any nozzle size.
|
||
func WriteStencilSCAD(filename string, gf *GerberFile, outlineGf *GerberFile, cfg Config, bounds *Bounds) 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")
|
||
lw := cfg.LineWidth
|
||
fmt.Fprintf(f, "stencil_height = %f; // mm – solder paste layer thickness\n", snapToLine(cfg.StencilHeight, lw))
|
||
fmt.Fprintf(f, "wall_height = %f; // mm – alignment frame height\n", snapToLine(cfg.WallHeight, lw))
|
||
fmt.Fprintf(f, "wall_thickness = %f; // mm – alignment frame wall thickness\n", snapToLine(cfg.WallThickness, lw))
|
||
if lw > 0 {
|
||
fmt.Fprintf(f, "// line_width = %f; // mm – all dimensions snapped to multiples/fractions of this\n", lw)
|
||
}
|
||
fmt.Fprintf(f, "\n")
|
||
|
||
var outlineVerts [][2]float64
|
||
if outlineGf != nil {
|
||
outlineVerts = ExtractPolygonFromGerber(outlineGf)
|
||
}
|
||
|
||
centerX := (bounds.MinX + bounds.MaxX) / 2.0
|
||
centerY := (bounds.MinY + bounds.MaxY) / 2.0
|
||
|
||
// Board outline module (2D)
|
||
if len(outlineVerts) > 0 {
|
||
fmt.Fprintf(f, "module board_outline() {\n polygon(points=[\n")
|
||
for i, v := range outlineVerts {
|
||
fmt.Fprintf(f, " [%f, %f]", v[0], v[1])
|
||
if i < len(outlineVerts)-1 {
|
||
fmt.Fprintf(f, ",")
|
||
}
|
||
fmt.Fprintf(f, "\n")
|
||
}
|
||
fmt.Fprintf(f, " ]);\n}\n\n")
|
||
} else {
|
||
// Fallback: bounding rectangle
|
||
fmt.Fprintf(f, "module board_outline() {\n")
|
||
fmt.Fprintf(f, " translate([%f, %f]) square([%f, %f]);\n",
|
||
bounds.MinX, bounds.MinY, bounds.MaxX-bounds.MinX, bounds.MaxY-bounds.MinY)
|
||
fmt.Fprintf(f, "}\n\n")
|
||
}
|
||
|
||
// Paste pad openings module (2D union of all aperture shapes)
|
||
fmt.Fprintf(f, "module paste_pads() {\n union() {\n")
|
||
writeGerberShapes2D(f, gf, cfg.LineWidth, " ")
|
||
fmt.Fprintf(f, " }\n}\n\n")
|
||
|
||
// Main body – centred at origin for easy placement on the print bed
|
||
fmt.Fprintf(f, "translate([%f, %f, 0]) {\n", -centerX, -centerY)
|
||
fmt.Fprintf(f, " difference() {\n")
|
||
fmt.Fprintf(f, " union() {\n")
|
||
fmt.Fprintf(f, " // Thin stencil plate\n")
|
||
fmt.Fprintf(f, " linear_extrude(height=stencil_height)\n")
|
||
fmt.Fprintf(f, " board_outline();\n")
|
||
fmt.Fprintf(f, " // Alignment wall – keeps stencil registered to the PCB edge\n")
|
||
fmt.Fprintf(f, " linear_extrude(height=wall_height)\n")
|
||
fmt.Fprintf(f, " difference() {\n")
|
||
fmt.Fprintf(f, " offset(r=wall_thickness) board_outline();\n")
|
||
fmt.Fprintf(f, " board_outline();\n")
|
||
fmt.Fprintf(f, " }\n")
|
||
fmt.Fprintf(f, " }\n")
|
||
fmt.Fprintf(f, " // Paste pad cutouts (punched through the stencil plate)\n")
|
||
fmt.Fprintf(f, " translate([0, 0, -0.1])\n")
|
||
fmt.Fprintf(f, " linear_extrude(height=stencil_height + 0.2)\n")
|
||
fmt.Fprintf(f, " paste_pads();\n")
|
||
fmt.Fprintf(f, " }\n")
|
||
fmt.Fprintf(f, "}\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, lidCutouts []LidCutout, 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. UI specifies c.Y from bottom, so c.Y adds to Z.
|
||
z := c.Height/2 + trayFloor + pcbT + c.Y
|
||
wallDepth := 2*(clearance+2*wt) + 2.0 // just enough to cut through walls
|
||
w, d, h := c.Width, wallDepth, c.Height
|
||
|
||
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
|
||
|
||
if c.CornerRadius > 0 {
|
||
r := c.CornerRadius
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) rotate([0, 0, %f]) {\n", midX, midY, z, rotDeg)
|
||
fmt.Fprintf(f, " hull() {\n")
|
||
fmt.Fprintf(f, " translate([%f, 0, %f]) rotate([90, 0, 0]) cylinder(r=%f, h=%f, center=true);\n", w/2-r, h/2-r, r, d)
|
||
fmt.Fprintf(f, " translate([%f, 0, %f]) rotate([90, 0, 0]) cylinder(r=%f, h=%f, center=true);\n", -(w/2 - r), h/2-r, r, d)
|
||
fmt.Fprintf(f, " translate([%f, 0, %f]) rotate([90, 0, 0]) cylinder(r=%f, h=%f, center=true);\n", w/2-r, -(h/2 - r), r, d)
|
||
fmt.Fprintf(f, " translate([%f, 0, %f]) rotate([90, 0, 0]) cylinder(r=%f, h=%f, center=true);\n", -(w/2 - r), -(h/2 - r), r, d)
|
||
fmt.Fprintf(f, " }\n")
|
||
fmt.Fprintf(f, " }\n")
|
||
} else {
|
||
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")
|
||
|
||
// Print Pry Clips Module
|
||
fmt.Fprintf(f, "module pry_clips() {\n")
|
||
clipH := 0.8
|
||
clipZ := trayFloor + snapHeight - clipH/2.0
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) cube([%f, %f, %f], center=true);\n", minBX-clearance-wt-0.5, boardCenterY, clipZ, 1.0, pryW, clipH)
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) cube([%f, %f, %f], center=true);\n", maxBX+clearance+wt+0.5, boardCenterY, clipZ, 1.0, pryW, clipH)
|
||
fmt.Fprintf(f, "}\n\n")
|
||
|
||
// Lid/Tray Cutouts Module
|
||
fmt.Fprintf(f, "module lid_cutouts() {\n")
|
||
for _, lc := range lidCutouts {
|
||
cx := (lc.MinX + lc.MaxX) / 2.0
|
||
cy := (lc.MinY + lc.MaxY) / 2.0
|
||
w := lc.MaxX - lc.MinX
|
||
h := lc.MaxY - lc.MinY
|
||
if w < 0.01 || h < 0.01 {
|
||
continue
|
||
}
|
||
if lc.Plane == "lid" {
|
||
if lc.IsDado && lc.Depth > 0 {
|
||
// Dado on lid: cut from top surface downward
|
||
fmt.Fprintf(f, " // Lid dado (depth=%.2f)\n", lc.Depth)
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) cube([%f, %f, %f], center=true);\n",
|
||
cx, cy, totalH-lc.Depth/2.0, w, h, lc.Depth+0.1)
|
||
} else {
|
||
// Through-cut on lid
|
||
fmt.Fprintf(f, " // Lid through-cut\n")
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) cube([%f, %f, %f], center=true);\n",
|
||
cx, cy, totalH-lidThick/2.0, w, h, lidThick+0.2)
|
||
}
|
||
} else if lc.Plane == "tray" {
|
||
if lc.IsDado && lc.Depth > 0 {
|
||
// Dado on tray: cut from bottom surface upward
|
||
fmt.Fprintf(f, " // Tray dado (depth=%.2f)\n", lc.Depth)
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) cube([%f, %f, %f], center=true);\n",
|
||
cx, cy, lc.Depth/2.0-0.05, w, h, lc.Depth+0.1)
|
||
} else {
|
||
// Through-cut on tray floor
|
||
fmt.Fprintf(f, " // Tray through-cut\n")
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) cube([%f, %f, %f], center=true);\n",
|
||
cx, cy, trayFloor/2.0, w, h, trayFloor+0.2)
|
||
}
|
||
}
|
||
}
|
||
fmt.Fprintf(f, "}\n\n")
|
||
|
||
// cutoutMid returns the midpoint XY and rotation angle for a side cutout,
|
||
// matching the geometry used in side_cutouts().
|
||
cutoutMid := func(c SideCutout) (midX, midY, rotDeg float64, ok bool) {
|
||
for i := range sides {
|
||
if sides[i].Num != c.Side {
|
||
continue
|
||
}
|
||
bs := &sides[i]
|
||
dx := bs.EndX - bs.StartX
|
||
dy := bs.EndY - bs.StartY
|
||
if l := math.Sqrt(dx*dx + dy*dy); l > 0 {
|
||
dx /= l
|
||
dy /= l
|
||
}
|
||
midX = bs.StartX + dx*(c.X+c.Width/2)
|
||
midY = bs.StartY + dy*(c.X+c.Width/2)
|
||
rotDeg = (bs.Angle*180.0/math.Pi) - 90.0
|
||
ok = true
|
||
return
|
||
}
|
||
return
|
||
}
|
||
|
||
centerX := cfg.OutlineBounds.MinX + (cfg.OutlineBounds.MaxX-cfg.OutlineBounds.MinX)/2.0
|
||
centerY := cfg.OutlineBounds.MinY + (cfg.OutlineBounds.MaxY-cfg.OutlineBounds.MinY)/2.0
|
||
fmt.Fprintf(f, "translate([%f, %f, 0]) {\n", -centerX, -centerY)
|
||
|
||
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, " }\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, " side_cutouts();\n")
|
||
fmt.Fprintf(f, " lid_cutouts();\n")
|
||
// Board dado on tray: layer-aware groove on each side with port cutouts.
|
||
{
|
||
trayWallDepth := 2*(clearance+wt) + 2.0
|
||
type trayDadoInfo struct {
|
||
hasF bool
|
||
hasB bool
|
||
fPortTop float64
|
||
bPortBot float64
|
||
}
|
||
trayDadoSides := make(map[int]*trayDadoInfo)
|
||
for _, c := range cutouts {
|
||
di, ok := trayDadoSides[c.Side]
|
||
if !ok {
|
||
di = &trayDadoInfo{fPortTop: 0, bPortBot: 1e9}
|
||
trayDadoSides[c.Side] = di
|
||
}
|
||
portBot := trayFloor + pcbT + c.Y
|
||
portTop := portBot + c.Height
|
||
if c.Layer == "F" {
|
||
di.hasF = true
|
||
if portTop > di.fPortTop {
|
||
di.fPortTop = portTop
|
||
}
|
||
} else {
|
||
di.hasB = true
|
||
if portBot < di.bPortBot {
|
||
di.bPortBot = portBot
|
||
}
|
||
}
|
||
}
|
||
trayH := trayFloor + snapHeight + wt + pcbT + 2.0
|
||
for _, bs := range sides {
|
||
di, ok := trayDadoSides[bs.Num]
|
||
if !ok {
|
||
continue
|
||
}
|
||
midX := (bs.StartX + bs.EndX) / 2.0
|
||
midY := (bs.StartY + bs.EndY) / 2.0
|
||
rotDeg := (bs.Angle*180.0/math.Pi) - 90.0
|
||
dadoLen := bs.Length + 1.0
|
||
if di.hasF {
|
||
// F-layer: dado above ports (toward lid), same direction as enclosure
|
||
dadoBot := di.fPortTop
|
||
dadoH := trayH - dadoBot
|
||
if dadoH > 0.1 {
|
||
fmt.Fprintf(f, " // Board dado (F-layer) on side %d\n", bs.Num)
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n",
|
||
midX, midY, dadoBot+dadoH/2.0, rotDeg, dadoLen, trayWallDepth, dadoH)
|
||
}
|
||
}
|
||
if di.hasB {
|
||
// B-layer: dado below ports (toward floor)
|
||
dadoBot := trayFloor + 0.3
|
||
dadoH := di.bPortBot - dadoBot
|
||
if dadoH > 0.1 {
|
||
fmt.Fprintf(f, " // Board dado (B-layer) on side %d\n", bs.Num)
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n",
|
||
midX, midY, dadoBot+dadoH/2.0, rotDeg, dadoLen, trayWallDepth, dadoH)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
fmt.Fprintf(f, "}\n")
|
||
fmt.Fprintf(f, "pry_clips();\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, " // Vertical relief slots for the tray clips to slide into\n")
|
||
reliefClipZ := trayFloor + snapHeight
|
||
reliefH := reliefClipZ + 1.0
|
||
reliefZ := trayFloor - 1.0
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) cube([1.5, %f, %f], center=true);\n", minBX-clearance-wt-0.25, boardCenterY, reliefZ, pryW+1.0, reliefH)
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) cube([1.5, %f, %f], center=true);\n", maxBX+clearance+wt+0.25, boardCenterY, reliefZ, pryW+1.0, reliefH)
|
||
|
||
fmt.Fprintf(f, " pry_slots();\n")
|
||
|
||
// Port cutouts – only these go through the full wall to the outside
|
||
fmt.Fprintf(f, " side_cutouts();\n")
|
||
fmt.Fprintf(f, " lid_cutouts();\n")
|
||
|
||
wallDepth := 2*(clearance+2*wt) + 2.0
|
||
lidBottom := totalH - lidThick
|
||
|
||
// Inner wall ring helper – used to limit slots and dado to the
|
||
// inner rim only (outer wall stays solid, only ports break through).
|
||
// Inner wall spans from offset(clearance) to offset(clearance+wt).
|
||
fmt.Fprintf(f, " // --- Entry slots & board dado (inner wall only) ---\n")
|
||
fmt.Fprintf(f, " intersection() {\n")
|
||
fmt.Fprintf(f, " // Clamp to inner wall ring\n")
|
||
fmt.Fprintf(f, " translate([0,0,%f]) linear_extrude(height=%f) difference() {\n", trayFloor-1, totalH+2)
|
||
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance+wt+0.5)
|
||
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance-0.5)
|
||
fmt.Fprintf(f, " }\n")
|
||
fmt.Fprintf(f, " union() {\n")
|
||
|
||
// Port entry slots – vertical channel from port to lid/floor,
|
||
// only in the inner wall so the outer wall stays solid.
|
||
for _, c := range cutouts {
|
||
mX, mY, mRot, ok := cutoutMid(c)
|
||
if !ok {
|
||
continue
|
||
}
|
||
zTopCut := trayFloor + pcbT + c.Y + c.Height
|
||
|
||
if c.Layer == "F" {
|
||
// F-layer: ports on top of board, slot from port top toward lid (plate)
|
||
slotH := lidBottom - zTopCut
|
||
if slotH > 0.1 {
|
||
fmt.Fprintf(f, " // Port entry slot (F-layer, open toward plate)\n")
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n",
|
||
mX, mY, zTopCut+slotH/2.0, mRot, c.Width, wallDepth, slotH)
|
||
}
|
||
} else {
|
||
// B-layer: ports under board, slot from floor up to port bottom
|
||
zBotCut := trayFloor + pcbT + c.Y
|
||
slotH := zBotCut - (trayFloor + 0.3)
|
||
if slotH > 0.1 {
|
||
slotBot := trayFloor + 0.3
|
||
fmt.Fprintf(f, " // Port entry slot (B-layer, open toward rim)\n")
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n",
|
||
mX, mY, slotBot+slotH/2.0, mRot, c.Width, wallDepth, slotH)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Board dado – full-length groove at PCB height, inner wall only.
|
||
// For F-layer: dado sits below ports (board under ports), from tray floor to port bottom.
|
||
// For B-layer: dado sits above ports (board over ports), from port top to lid.
|
||
// Collect per-side: lowest port bottom (F) or highest port top (B).
|
||
type dadoInfo struct {
|
||
hasF bool
|
||
hasB bool
|
||
fPortTop float64 // highest port-top on this side (F-layer)
|
||
bPortBot float64 // lowest port-bottom on this side (B-layer)
|
||
}
|
||
dadoSides := make(map[int]*dadoInfo)
|
||
for _, c := range cutouts {
|
||
di, ok := dadoSides[c.Side]
|
||
if !ok {
|
||
di = &dadoInfo{fPortTop: 0, bPortBot: 1e9}
|
||
dadoSides[c.Side] = di
|
||
}
|
||
portBot := trayFloor + pcbT + c.Y
|
||
portTop := portBot + c.Height
|
||
if c.Layer == "F" {
|
||
di.hasF = true
|
||
if portTop > di.fPortTop {
|
||
di.fPortTop = portTop
|
||
}
|
||
} else {
|
||
di.hasB = true
|
||
if portBot < di.bPortBot {
|
||
di.bPortBot = portBot
|
||
}
|
||
}
|
||
}
|
||
for _, bs := range sides {
|
||
di, ok := dadoSides[bs.Num]
|
||
if !ok {
|
||
continue
|
||
}
|
||
midX := (bs.StartX + bs.EndX) / 2.0
|
||
midY := (bs.StartY + bs.EndY) / 2.0
|
||
rotDeg := (bs.Angle*180.0/math.Pi) - 90.0
|
||
dadoLen := bs.Length + 1.0
|
||
if di.hasF {
|
||
// F-layer: ports on top of board, dado above ports (toward lid/plate)
|
||
dadoBot := di.fPortTop
|
||
dadoH := lidBottom - dadoBot
|
||
if dadoH > 0.1 {
|
||
fmt.Fprintf(f, " // Board dado (F-layer) on side %d\n", bs.Num)
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n",
|
||
midX, midY, dadoBot+dadoH/2.0, rotDeg, dadoLen, wallDepth, dadoH)
|
||
}
|
||
}
|
||
if di.hasB {
|
||
// B-layer: ports under board, dado below ports (toward open rim)
|
||
dadoBot := trayFloor + 0.3
|
||
dadoH := di.bPortBot - dadoBot
|
||
if dadoH > 0.1 {
|
||
fmt.Fprintf(f, " // Board dado (B-layer) on side %d\n", bs.Num)
|
||
fmt.Fprintf(f, " translate([%f, %f, %f]) rotate([0, 0, %f]) cube([%f, %f, %f], center=true);\n",
|
||
midX, midY, dadoBot+dadoH/2.0, rotDeg, dadoLen, wallDepth, dadoH)
|
||
}
|
||
}
|
||
}
|
||
|
||
fmt.Fprintf(f, " } // end union\n")
|
||
fmt.Fprintf(f, " } // end intersection\n")
|
||
fmt.Fprintf(f, "}\n")
|
||
fmt.Fprintf(f, "mounting_pegs(false);\n")
|
||
}
|
||
|
||
fmt.Fprintf(f, "}\n") // Close the top-level translate
|
||
|
||
return nil
|
||
}
|