This commit is contained in:
pszsh 2026-02-23 13:16:14 -08:00
parent cf424d4611
commit 66ea1db755
5 changed files with 280 additions and 65 deletions

Binary file not shown.

View File

@ -513,8 +513,28 @@ func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg Enclo
bw := float64(x-runStartX) * pixelToMM bw := float64(x-runStartX) * pixelToMM
bh := pixelToMM bh := pixelToMM
isSpringRelief := false
if !curIsInner && bx >= float64(minBX)*pixelToMM-clearance-wt-1.0 && bx <= float64(maxBX)*pixelToMM+clearance+wt+1.0 {
// Check if the current pixel run constitutes either the left or right clip relief
pryWMM := 8.0
by_center := float64(int(boardCenterY/float64(boardCount))) * pixelToMM
leftClipX := float64(minBX)*pixelToMM - clearance - wt
rightClipX := float64(maxBX)*pixelToMM + clearance + wt
if by >= by_center-pryWMM/2.0-0.5 && by <= by_center+pryWMM/2.0+0.5 {
if math.Abs(bx-leftClipX) <= 1.5 || math.Abs(bx-rightClipX) <= 1.5 {
isSpringRelief = true
}
}
}
if curIsInner { if curIsInner {
addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight, bw, bh, totalH-(trayFloor+snapHeight)) addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight, bw, bh, totalH-(trayFloor+snapHeight))
} else {
if isSpringRelief {
// For relief wall cut, omit the bottom solid wall material from the tray floor
addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight+1.0, bw, bh, totalH-(trayFloor+snapHeight+1.0))
} else if curIsSnap { } else if curIsSnap {
// Snap groove: remove material from (trayFloor+snapHeight-0.7) to (trayFloor+snapHeight-0.1) // Snap groove: remove material from (trayFloor+snapHeight-0.7) to (trayFloor+snapHeight-0.1)
addBoxAtZ(&encTris, bx, by, trayFloor, bw, bh, snapHeight-0.7) addBoxAtZ(&encTris, bx, by, trayFloor, bw, bh, snapHeight-0.7)
@ -523,6 +543,7 @@ func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg Enclo
// Outer wall // Outer wall
addBoxAtZ(&encTris, bx, by, trayFloor, bw, bh, totalH-trayFloor) addBoxAtZ(&encTris, bx, by, trayFloor, bw, bh, totalH-trayFloor)
} }
}
runStartX = x runStartX = x
curIsInner = isInnerWall curIsInner = isInnerWall
@ -535,14 +556,34 @@ func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg Enclo
bw := float64(x-runStartX) * pixelToMM bw := float64(x-runStartX) * pixelToMM
bh := pixelToMM bh := pixelToMM
isSpringRelief := false
if !curIsInner && bx >= float64(minBX)*pixelToMM-clearance-wt-1.0 && bx <= float64(maxBX)*pixelToMM+clearance+wt+1.0 {
pryWMM := 8.0
by_center := float64(int(boardCenterY/float64(boardCount))) * pixelToMM
leftClipX := float64(minBX)*pixelToMM - clearance - wt
rightClipX := float64(maxBX)*pixelToMM + clearance + wt
if by >= by_center-pryWMM/2.0-0.5 && by <= by_center+pryWMM/2.0+0.5 {
if math.Abs(bx-leftClipX) <= 1.5 || math.Abs(bx-rightClipX) <= 1.5 {
isSpringRelief = true
}
}
}
if curIsInner { if curIsInner {
addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight, bw, bh, totalH-(trayFloor+snapHeight)) addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight, bw, bh, totalH-(trayFloor+snapHeight))
} else {
if isSpringRelief {
// For relief wall cut, omit the bottom solid wall material from the tray floor
addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight+1.0, bw, bh, totalH-(trayFloor+snapHeight+1.0))
} else if curIsSnap { } else if curIsSnap {
addBoxAtZ(&encTris, bx, by, trayFloor, bw, bh, snapHeight-0.7) addBoxAtZ(&encTris, bx, by, trayFloor, bw, bh, snapHeight-0.7)
addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight-0.1, bw, bh, totalH-(trayFloor+snapHeight-0.1)) addBoxAtZ(&encTris, bx, by, trayFloor+snapHeight-0.1, bw, bh, totalH-(trayFloor+snapHeight-0.1))
} else { } else {
addBoxAtZ(&encTris, bx, by, trayFloor, bw, bh, totalH-trayFloor) addBoxAtZ(&encTris, bx, by, trayFloor, bw, bh, totalH-trayFloor)
} }
}
runStartX = -1 runStartX = -1
} }
} }
@ -581,6 +622,7 @@ func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg Enclo
sideNum := -1 sideNum := -1
minDist := math.MaxFloat64 minDist := math.MaxFloat64
var bestPosAlongSide float64
for _, bs := range boardSides { for _, bs := range boardSides {
dx := bs.EndX - bs.StartX dx := bs.EndX - bs.StartX
dy := bs.EndY - bs.StartY dy := bs.EndY - bs.StartY
@ -596,6 +638,7 @@ func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg Enclo
if dist < minDist { if dist < minDist {
minDist = dist minDist = dist
sideNum = bs.Num sideNum = bs.Num
bestPosAlongSide = t * bs.Length
} }
} }
@ -609,14 +652,18 @@ func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg Enclo
if c.Side != sideNum { if c.Side != sideNum {
continue continue
} }
// Wall below cutout: from 0 to cutout.Y
if c.Y > 0.1 { minZ, maxZ := cutoutZBounds(c, bestPosAlongSide)
addBoxAtZ(&cutoutEncTris, bx2, by2, 0, bw, bh, c.Y) minZ += trayFloor + pcbT
maxZ += trayFloor + pcbT
// Wall below cutout: from 0 to minZ
if minZ > 0.05 {
addBoxAtZ(&cutoutEncTris, bx2, by2, 0, bw, bh, minZ)
} }
// Wall above cutout: from cutout.Y+cutout.H to totalH // Wall above cutout: from maxZ to totalH
cutTop := c.Y + c.Height if maxZ < totalH-0.05 {
if cutTop < totalH-0.1 { addBoxAtZ(&cutoutEncTris, bx2, by2, maxZ, bw, bh, totalH-maxZ)
addBoxAtZ(&cutoutEncTris, bx2, by2, cutTop, bw, bh, totalH-cutTop)
} }
break break
} }
@ -736,11 +783,9 @@ func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg Enclo
for y := 0; y < imgH; y++ { for y := 0; y < imgH; y++ {
runStartX := -1 runStartX := -1
curIsWall := false curIsWall := false
curIsBump := false
for x := 0; x <= imgW; x++ { for x := 0; x <= imgW; x++ {
isTrayFloor := false isTrayFloor := false
isTrayWall := false isTrayWall := false
isTrayBump := false
if x < imgW { if x < imgW {
idx := y*imgW + x idx := y*imgW + x
if !pegMask[idx] { if !pegMask[idx] {
@ -753,10 +798,6 @@ func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg Enclo
if dist > clearanceDistPx && dist <= trayWallOuterPx && !boardMask[idx] { if dist > clearanceDistPx && dist <= trayWallOuterPx && !boardMask[idx] {
isTrayWall = true isTrayWall = true
} }
// Tray Bumps sit on the outside of the Tray Wall
if dist > trayWallOuterPx && dist <= trayWallOuterPx+snapDepthPx && !boardMask[idx] {
isTrayBump = true
}
} }
} }
@ -764,24 +805,76 @@ func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg Enclo
if runStartX == -1 { if runStartX == -1 {
runStartX = x runStartX = x
curIsWall = isTrayWall curIsWall = isTrayWall
curIsBump = isTrayBump } else if isTrayWall != curIsWall {
} else if isTrayWall != curIsWall || isTrayBump != curIsBump {
bx := float64(runStartX) * pixelToMM bx := float64(runStartX) * pixelToMM
by := float64(y) * pixelToMM by := float64(y) * pixelToMM
bw := float64(x-runStartX) * pixelToMM bw := float64(x-runStartX) * pixelToMM
bh := pixelToMM bh := pixelToMM
addBoxAtZ(&trayTris, bx, by, 0, bw, bh, trayFloor) addBoxAtZ(&trayTris, bx, by, 0, bw, bh, trayFloor)
if curIsWall {
addBoxAtZ(&trayTris, bx, by, trayFloor, bw, bh, snapHeight) wallBase := trayFloor
} else if curIsBump { wallH := snapHeight
// Adds a small 0.4mm bump on the outside of the wall
addBoxAtZ(&trayTris, bx, by, trayFloor+snapHeight-0.6, bw, bh, 0.4) // Evaluate cutout limits if this pixel run falls into a cutout mask
isCutout := false
for testX := runStartX; testX < x; testX++ {
if wallCutoutMask[y*imgW+testX] {
isCutout = true
break
}
}
if isCutout && len(sideCutouts) > 0 {
midX := (runStartX + x) / 2
bxMid := float64(midX)*pixelToMM + cfg.OutlineBounds.MinX
byMid := cfg.OutlineBounds.MaxY - float64(y)*pixelToMM
sideNum := -1
minDist := math.MaxFloat64
var bestPosAlongSide float64
for _, bs := range boardSides {
dx := bs.EndX - bs.StartX
dy := bs.EndY - bs.StartY
lenSq := dx*dx + dy*dy
if lenSq == 0 {
continue
}
t := ((bxMid-bs.StartX)*dx + (byMid-bs.StartY)*dy) / lenSq
tClamp := math.Max(0, math.Min(1, t))
projX := bs.StartX + tClamp*dx
projY := bs.StartY + tClamp*dy
dist := math.Sqrt((bxMid-projX)*(bxMid-projX) + (byMid-projY)*(byMid-projY))
if dist < minDist {
minDist = dist
sideNum = bs.Num
bestPosAlongSide = t * bs.Length
}
}
for _, c := range sideCutouts {
if c.Side == sideNum {
minZ, _ := cutoutZBounds(c, bestPosAlongSide)
minZ += trayFloor + pcbT
// Tray wall goes up to trayFloor + snapHeight. If minZ is lower, truncate it.
if minZ < trayFloor+wallH {
wallH = minZ - trayFloor
if wallH < 0 {
wallH = 0
}
}
break
}
}
}
if curIsWall && wallH > 0.05 {
addBoxAtZ(&trayTris, bx, by, wallBase, bw, bh, wallH)
} }
runStartX = x runStartX = x
curIsWall = isTrayWall curIsWall = isTrayWall
curIsBump = isTrayBump
} }
} else { } else {
if runStartX != -1 { if runStartX != -1 {
@ -791,10 +884,64 @@ func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg Enclo
bh := pixelToMM bh := pixelToMM
addBoxAtZ(&trayTris, bx, by, 0, bw, bh, trayFloor) addBoxAtZ(&trayTris, bx, by, 0, bw, bh, trayFloor)
if curIsWall {
addBoxAtZ(&trayTris, bx, by, trayFloor, bw, bh, snapHeight) wallBase := trayFloor
} else if curIsBump { wallH := snapHeight
addBoxAtZ(&trayTris, bx, by, trayFloor+snapHeight-0.6, bw, bh, 0.4)
// Evaluate cutout limits if this pixel run falls into a cutout mask
isCutout := false
for testX := runStartX; testX < x; testX++ {
if wallCutoutMask[y*imgW+testX] {
isCutout = true
break
}
}
if isCutout && len(sideCutouts) > 0 {
midX := (runStartX + x) / 2
bxMid := float64(midX)*pixelToMM + cfg.OutlineBounds.MinX
byMid := cfg.OutlineBounds.MaxY - float64(y)*pixelToMM
sideNum := -1
minDist := math.MaxFloat64
var bestPosAlongSide float64
for _, bs := range boardSides {
dx := bs.EndX - bs.StartX
dy := bs.EndY - bs.StartY
lenSq := dx*dx + dy*dy
if lenSq == 0 {
continue
}
t := ((bxMid-bs.StartX)*dx + (byMid-bs.StartY)*dy) / lenSq
tClamp := math.Max(0, math.Min(1, t))
projX := bs.StartX + tClamp*dx
projY := bs.StartY + tClamp*dy
dist := math.Sqrt((bxMid-projX)*(bxMid-projX) + (byMid-projY)*(byMid-projY))
if dist < minDist {
minDist = dist
sideNum = bs.Num
bestPosAlongSide = t * bs.Length
}
}
for _, c := range sideCutouts {
if c.Side == sideNum {
minZ, _ := cutoutZBounds(c, bestPosAlongSide)
minZ += trayFloor + pcbT
if minZ < trayFloor+wallH {
wallH = minZ - trayFloor
if wallH < 0 {
wallH = 0
}
}
break
}
}
}
if curIsWall && wallH > 0.05 {
addBoxAtZ(&trayTris, bx, by, wallBase, bw, bh, wallH)
} }
runStartX = -1 runStartX = -1
} }
@ -802,11 +949,40 @@ func GenerateEnclosure(outlineImg image.Image, drillHoles []DrillHole, cfg Enclo
} }
} }
// (Old PCB support rim, snap bump, embossed lip, and removal tab loops have been permanently removed because the Tray geometry forms a flush fitting bottom shoe-box lid interface) // Add Pry Clips to the Tray to sit under the Enclosure Pry Slots
fmt.Printf("Enclosure: %d triangles, Tray: %d triangles\n", len(encTris), len(trayTris)) if boardCount > 0 {
pryWMM := 8.0
pryDMM := 1.0
clipH := 0.8
leftX := float64(minBX)*pixelToMM - clearance - wt
rightX := float64(maxBX)*pixelToMM + clearance + wt
by_center := float64(int(boardCenterY/float64(boardCount))) * pixelToMM
// Z coordinates: trayFloor + snapHeight - clipH ensures the clip finishes flush with the top of the tray wall
addBoxAtZ(&trayTris, leftX-pryDMM, by_center-pryWMM/2.0, trayFloor+snapHeight-clipH, pryDMM, pryWMM, clipH)
addBoxAtZ(&trayTris, rightX, by_center-pryWMM/2.0, trayFloor+snapHeight-clipH, pryDMM, pryWMM, clipH)
}
_ = math.Pi // keep math import for Phase 2 cylindrical pegs _ = math.Pi // keep math import for Phase 2 cylindrical pegs
// Shift meshes to origin so the exported STL is centered
offsetX := float64(imgW) * pixelToMM / 2.0
offsetY := float64(imgH) * pixelToMM / 2.0
for i := range encTris {
for j := 0; j < 3; j++ {
encTris[i][j].X -= offsetX
encTris[i][j].Y -= offsetY
}
}
for i := range trayTris {
for j := 0; j < 3; j++ {
trayTris[i][j].X -= offsetX
trayTris[i][j].Y -= offsetY
}
}
return &EnclosureResult{ return &EnclosureResult{
EnclosureTriangles: encTris, EnclosureTriangles: encTris,
TrayTriangles: trayTris, TrayTriangles: trayTris,
@ -941,3 +1117,27 @@ func floodFillExterior(pixels []bool, w, h int) []bool {
return exterior return exterior
} }
// cutoutZBounds calculates the accurate Z bounds taking into account corner radii
func cutoutZBounds(c SideCutout, posAlongSide float64) (float64, float64) {
minZ := c.Y
maxZ := c.Y + c.Height
if c.CornerRadius > 0 {
r := c.CornerRadius
localX := posAlongSide - c.X
if localX < r {
dx := r - localX
dy := r - math.Sqrt(math.Max(0, r*r-dx*dx))
minZ += dy
maxZ -= dy
} else if localX > c.Width-r {
dx := localX - (c.Width - r)
dy := r - math.Sqrt(math.Max(0, r*r-dx*dx))
minZ += dy
maxZ -= dy
}
}
return minZ, maxZ
}

View File

@ -988,6 +988,7 @@ func enclosureUploadHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
pixelToMM := 25.4 / ecfg.DPI
session := &EnclosureSession{ session := &EnclosureSession{
Exports: r.Form["exports"], Exports: r.Form["exports"],
OutlineGf: outlineGf, OutlineGf: outlineGf,
@ -1000,10 +1001,10 @@ func enclosureUploadHandler(w http.ResponseWriter, r *http.Request) {
BoardW: actualBoardW, BoardW: actualBoardW,
BoardH: actualBoardH, BoardH: actualBoardH,
TotalH: ecfg.WallHeight + ecfg.PCBThickness + 1.0, TotalH: ecfg.WallHeight + ecfg.PCBThickness + 1.0,
MinBX: float64(minBX), MinBX: float64(minBX)*pixelToMM + outlineBounds.MinX,
MaxBX: float64(maxBX), MaxBX: float64(maxBX)*pixelToMM + outlineBounds.MinX,
BoardCenterY: boardCenterY, BoardCenterY: outlineBounds.MaxY - boardCenterY*pixelToMM,
Sides: ExtractBoardSidesFromMask(boardMask, imgW, imgH, 25.4/ecfg.DPI, &outlineBounds), Sides: ExtractBoardSidesFromMask(boardMask, imgW, imgH, pixelToMM, &outlineBounds),
} }
sessionsMu.Lock() sessionsMu.Lock()
sessions[uuid] = session sessions[uuid] = session

57
scad.go
View File

@ -270,8 +270,8 @@ func WriteNativeSCAD(filename string, isTray bool, outlineVertices [][2]float64,
continue continue
} }
// Cutouts are relative to board. // Cutouts are relative to board. UI specifies c.Y from bottom, so c.Y adds to Z.
z := c.Height/2 + trayFloor + pcbT z := c.Height/2 + trayFloor + pcbT + c.Y
w, d, h := c.Width, 20.0, c.Height // d is deep enough to cut through walls w, d, h := c.Width, 20.0, c.Height // d is deep enough to cut through walls
dx := bs.EndX - bs.StartX dx := bs.EndX - bs.StartX
@ -287,8 +287,20 @@ func WriteNativeSCAD(filename string, isTray bool, outlineVertices [][2]float64,
rotDeg := (bs.Angle * 180.0 / math.Pi) - 90.0 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, " 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") fmt.Fprintf(f, "}\n\n")
// Print Pry Slots Module // Print Pry Slots Module
@ -299,6 +311,18 @@ func WriteNativeSCAD(filename string, isTray bool, outlineVertices [][2]float64,
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, " 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") 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")
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 { if isTray {
// --- TRAY --- // --- TRAY ---
fmt.Fprintf(f, "// --- TRAY ---\n") fmt.Fprintf(f, "// --- TRAY ---\n")
@ -311,19 +335,6 @@ func WriteNativeSCAD(filename string, isTray bool, outlineVertices [][2]float64,
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance+wt) fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance+wt)
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance) fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance)
fmt.Fprintf(f, " }\n") 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, " }\n")
fmt.Fprintf(f, " // Subtract Lip Recess (for easy opening)\n") fmt.Fprintf(f, " // Subtract Lip Recess (for easy opening)\n")
@ -340,9 +351,9 @@ func WriteNativeSCAD(filename string, isTray bool, outlineVertices [][2]float64,
socketRadius := (hole.Diameter / 2.0) + 0.1 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, " 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, " side_cutouts();\n")
fmt.Fprintf(f, "}\n\n") fmt.Fprintf(f, "}\n")
fmt.Fprintf(f, "pry_clips();\n\n")
} else { } else {
// --- ENCLOSURE --- // --- ENCLOSURE ---
@ -357,16 +368,18 @@ func WriteNativeSCAD(filename string, isTray bool, outlineVertices [][2]float64,
fmt.Fprintf(f, " // Subtract Tray Recess (Accommodates Tray Wall)\n") 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, " 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, " // Vertical relief slots for the tray clips to slide into\n")
fmt.Fprintf(f, " translate([0,0,%f]) linear_extrude(height=%f) difference() {\n", trayFloor+snapHeight-0.7, 0.6) fmt.Fprintf(f, " clipZ = %f;\n", trayFloor+snapHeight)
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance+wt+0.5) fmt.Fprintf(f, " translate([%f, %f, trayFloor - 1]) cube([1.5, %f, clipZ+1], center=true);\n", minBX-clearance-wt-0.25, boardCenterY, pryW+1.0)
fmt.Fprintf(f, " offset(r=%f) board_polygon();\n", clearance+wt) fmt.Fprintf(f, " translate([%f, %f, trayFloor - 1]) cube([1.5, %f, clipZ+1], center=true);\n", maxBX+clearance+wt+0.25, boardCenterY, pryW+1.0)
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, " pry_slots();\n") fmt.Fprintf(f, " pry_slots();\n")
fmt.Fprintf(f, " side_cutouts();\n") fmt.Fprintf(f, " side_cutouts();\n")
fmt.Fprintf(f, "}\n") fmt.Fprintf(f, "}\n")
fmt.Fprintf(f, "mounting_pegs(false);\n") fmt.Fprintf(f, "mounting_pegs(false);\n")
} }
fmt.Fprintf(f, "}\n") // Close the top-level translate
return nil return nil
} }

View File

@ -27,7 +27,8 @@
<span>Export</span> <span>Export</span>
<div class="menu-dropdown export-options"> <div class="menu-dropdown export-options">
<label><input type="checkbox" name="export-stl" value="stl" checked> STL (3D Mesh)</label> <label><input type="checkbox" name="export-stl" value="stl" checked> STL (3D Mesh)</label>
<label><input type="checkbox" name="export-scad" value="scad"> SCAD (Native OpenSCAD)</label> <label><input type="checkbox" name="export-scad" value="scad" checked> SCAD (Native
OpenSCAD)</label>
<label><input type="checkbox" name="export-svg" value="svg"> SVG (2D Vector)</label> <label><input type="checkbox" name="export-svg" value="svg"> SVG (2D Vector)</label>
<label><input type="checkbox" name="export-png" value="png"> PNG (2D Raster)</label> <label><input type="checkbox" name="export-png" value="png"> PNG (2D Raster)</label>
</div> </div>