Former/unwrap.go

388 lines
9.6 KiB
Go

package main
import (
"fmt"
"math"
"strings"
)
type TabSlot struct {
X float64 `json:"x"`
Y float64 `json:"y"`
W float64 `json:"w"`
H float64 `json:"h"`
}
type UnwrapPanel struct {
Label string `json:"label"`
FaceType string `json:"faceType"` // "lid", "tray", "side"
SideNum int `json:"sideNum"`
X float64 `json:"x"`
Y float64 `json:"y"`
Width float64 `json:"width"`
Height float64 `json:"height"`
Angle float64 `json:"angle"`
Cutouts []Cutout `json:"cutouts"`
TabSlots []TabSlot `json:"tabSlots"`
// Polygon outline for non-rectangular panels (lid/tray)
Polygon [][2]float64 `json:"polygon,omitempty"`
}
type UnwrapLayout struct {
Panels []UnwrapPanel `json:"panels"`
TotalW float64 `json:"totalW"`
TotalH float64 `json:"totalH"`
WallThick float64 `json:"wallThick"`
}
// offsetPolygon moves each vertex outward along the average of its two adjacent edge normals.
func offsetPolygon(poly [][2]float64, dist float64) [][2]float64 {
n := len(poly)
if n < 3 {
return poly
}
// Remove closing duplicate if present
last := poly[n-1]
first := poly[0]
closed := math.Abs(last[0]-first[0]) < 0.01 && math.Abs(last[1]-first[1]) < 0.01
if closed {
n--
}
// Compute centroid to determine winding
cx, cy := 0.0, 0.0
for i := 0; i < n; i++ {
cx += poly[i][0]
cy += poly[i][1]
}
cx /= float64(n)
cy /= float64(n)
// Compute signed area to detect winding direction
area := 0.0
for i := 0; i < n; i++ {
j := (i + 1) % n
area += poly[i][0]*poly[j][1] - poly[j][0]*poly[i][1]
}
sign := 1.0
if area > 0 {
sign = -1.0
}
out := make([][2]float64, n)
for i := 0; i < n; i++ {
prev := (i - 1 + n) % n
next := (i + 1) % n
// Edge vectors
e1x := poly[i][0] - poly[prev][0]
e1y := poly[i][1] - poly[prev][1]
e2x := poly[next][0] - poly[i][0]
e2y := poly[next][1] - poly[i][1]
// Outward normals (perpendicular, pointing away from center)
n1x := -e1y * sign
n1y := e1x * sign
n2x := -e2y * sign
n2y := e2x * sign
// Normalize
l1 := math.Sqrt(n1x*n1x + n1y*n1y)
l2 := math.Sqrt(n2x*n2x + n2y*n2y)
if l1 > 0 {
n1x /= l1
n1y /= l1
}
if l2 > 0 {
n2x /= l2
n2y /= l2
}
// Average normal
ax := (n1x + n2x) / 2
ay := (n1y + n2y) / 2
al := math.Sqrt(ax*ax + ay*ay)
if al < 1e-9 {
out[i] = poly[i]
continue
}
ax /= al
ay /= al
// Half-angle cosine
dot := n1x*n2x + n1y*n2y
halfAngle := math.Acos(math.Max(-1, math.Min(1, dot))) / 2
cosHalf := math.Cos(halfAngle)
if cosHalf < 0.1 {
cosHalf = 0.1
}
scale := dist / cosHalf
out[i] = [2]float64{poly[i][0] + ax*scale, poly[i][1] + ay*scale}
}
// Close the polygon
out = append(out, out[0])
return out
}
// ComputeUnwrapLayout generates a flat unfolded net of the enclosure's outer surface.
func ComputeUnwrapLayout(session *EnclosureSession, cutouts []Cutout) *UnwrapLayout {
cfg := session.Config
wt := cfg.WallThickness
clearance := cfg.Clearance
offset := clearance + 2*wt
outlinePoly := ExtractPolygonFromGerber(session.OutlineGf)
if len(outlinePoly) < 3 {
return nil
}
lidPoly := offsetPolygon(outlinePoly, offset)
// Bounding box of the lid polygon
lidMinX, lidMinY := math.Inf(1), math.Inf(1)
lidMaxX, lidMaxY := math.Inf(-1), math.Inf(-1)
for _, p := range lidPoly {
if p[0] < lidMinX { lidMinX = p[0] }
if p[1] < lidMinY { lidMinY = p[1] }
if p[0] > lidMaxX { lidMaxX = p[0] }
if p[1] > lidMaxY { lidMaxY = p[1] }
}
lidW := lidMaxX - lidMinX
lidH := lidMaxY - lidMinY
wallH := cfg.WallHeight + cfg.PCBThickness + 1.0
sides := session.Sides
if len(sides) == 0 {
return nil
}
// Normalize lid polygon to origin for the SVG layout
normLidPoly := make([][2]float64, len(lidPoly))
for i, p := range lidPoly {
normLidPoly[i] = [2]float64{p[0] - lidMinX, p[1] - lidMinY}
}
// Classify cutouts by surface
sideCutouts, lidCutouts := SplitCutouts(cutouts, nil)
_ = lidCutouts
// Pry tab positions
pryW := 8.0
pryH := 1.5
// Find longest side for tray attachment
longestIdx := 0
longestLen := 0.0
for i, s := range sides {
if s.Length > longestLen {
longestLen = s.Length
longestIdx = i
}
}
// Layout: lid at center, sides folded out from lid edges
// We place lid at a starting position, then attach side panels along each edge.
// Simple cross layout: lid centered, sides extending outward.
margin := 5.0
layoutX := margin
layoutY := margin
var panels []UnwrapPanel
// Lid panel
lidPanel := UnwrapPanel{
Label: "L",
FaceType: "lid",
X: layoutX,
Y: layoutY + wallH, // sides above will fold up
Width: lidW,
Height: lidH,
Polygon: normLidPoly,
}
// Map lid cutouts
for _, c := range cutouts {
if c.Surface == "top" {
mapped := c
mapped.X = c.X - lidMinX + lidPanel.X
mapped.Y = c.Y - lidMinY + lidPanel.Y
lidPanel.Cutouts = append(lidPanel.Cutouts, mapped)
}
}
panels = append(panels, lidPanel)
// Side panels: attach along each edge of the lid
// For simplicity, we place sides in a strip below the lid, left to right
sideX := margin
sideY := lidPanel.Y + lidH // below the lid
for i, s := range sides {
sp := UnwrapPanel{
Label: fmt.Sprintf("S%d", s.Num),
FaceType: "side",
SideNum: s.Num,
X: sideX,
Y: sideY,
Width: s.Length,
Height: wallH,
Angle: s.Angle,
}
// Map side cutouts to this panel
for _, sc := range sideCutouts {
if sc.Side == s.Num {
mapped := Cutout{
ID: fmt.Sprintf("unwrap-s%d", sc.Side),
Surface: "side",
SideNum: sc.Side,
X: sc.X + sp.X,
Y: sc.Y + sp.Y,
Width: sc.Width,
Height: sc.Height,
}
sp.Cutouts = append(sp.Cutouts, mapped)
}
}
// Tab slots on pry tab sides
if s.Num == 1 || s.Num == len(sides) {
slot := TabSlot{
X: sp.Width/2 - pryW/2,
Y: 0,
W: pryW,
H: pryH,
}
sp.TabSlots = append(sp.TabSlots, slot)
}
panels = append(panels, sp)
// If this is the longest side, attach tray below it
if i == longestIdx {
trayPanel := UnwrapPanel{
Label: "T",
FaceType: "tray",
X: sideX,
Y: sideY + wallH,
Width: lidW,
Height: lidH,
Polygon: normLidPoly,
}
for _, c := range cutouts {
if c.Surface == "bottom" {
mapped := c
mapped.X = c.X - lidMinX + trayPanel.X
mapped.Y = c.Y - lidMinY + trayPanel.Y
trayPanel.Cutouts = append(trayPanel.Cutouts, mapped)
}
}
panels = append(panels, trayPanel)
}
sideX += s.Length + 2.0
}
// Compute total layout bounds
totalW := margin
totalH := margin
for _, p := range panels {
right := p.X + p.Width
bottom := p.Y + p.Height
if right > totalW { totalW = right }
if bottom > totalH { totalH = bottom }
}
totalW += margin
totalH += margin
return &UnwrapLayout{
Panels: panels,
TotalW: totalW,
TotalH: totalH,
WallThick: wt,
}
}
// GenerateUnwrapSVG produces an SVG string of the unfolded enclosure net.
func GenerateUnwrapSVG(layout *UnwrapLayout) string {
if layout == nil {
return ""
}
var b strings.Builder
b.WriteString(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %.2f %.2f" width="%.2fmm" height="%.2fmm">`+"\n",
layout.TotalW, layout.TotalH, layout.TotalW, layout.TotalH))
b.WriteString(`<style>`)
b.WriteString(`.panel { fill: none; stroke: #333; stroke-width: 0.3; }`)
b.WriteString(`.cutout { fill: #e0e0e0; stroke: #999; stroke-width: 0.2; }`)
b.WriteString(`.tab-slot { fill: #ccc; stroke: #999; stroke-width: 0.15; }`)
b.WriteString(`.fold-line { stroke: #aaa; stroke-width: 0.15; stroke-dasharray: 2,1; }`)
b.WriteString(`.label { font-family: sans-serif; font-size: 4px; fill: #666; text-anchor: middle; dominant-baseline: central; }`)
b.WriteString("</style>\n")
for _, p := range layout.Panels {
b.WriteString(fmt.Sprintf(`<g id="panel-%s" data-face="%s" data-side="%d" data-x="%.2f" data-y="%.2f" data-w="%.2f" data-h="%.2f">`+"\n",
p.Label, p.FaceType, p.SideNum, p.X, p.Y, p.Width, p.Height))
// Panel outline
if len(p.Polygon) > 2 {
b.WriteString(`<path class="panel" d="`)
for i, pt := range p.Polygon {
cmd := "L"
if i == 0 {
cmd = "M"
}
b.WriteString(fmt.Sprintf("%s%.3f,%.3f ", cmd, pt[0]+p.X, pt[1]+p.Y))
}
b.WriteString("Z\" />\n")
} else {
b.WriteString(fmt.Sprintf(`<rect class="panel" x="%.3f" y="%.3f" width="%.3f" height="%.3f" />`+"\n",
p.X, p.Y, p.Width, p.Height))
}
// Cutouts
for _, c := range p.Cutouts {
if c.Shape == "circle" {
r := c.Width / 2
b.WriteString(fmt.Sprintf(`<circle class="cutout" cx="%.3f" cy="%.3f" r="%.3f" />`+"\n",
c.X+r, c.Y+r, r))
} else {
b.WriteString(fmt.Sprintf(`<rect class="cutout" x="%.3f" y="%.3f" width="%.3f" height="%.3f" />`+"\n",
c.X, c.Y, c.Width, c.Height))
}
}
// Tab slots
for _, t := range p.TabSlots {
b.WriteString(fmt.Sprintf(`<rect class="tab-slot" x="%.3f" y="%.3f" width="%.3f" height="%.3f" />`+"\n",
p.X+t.X, p.Y+t.Y, t.W, t.H))
}
// Label
cx := p.X + p.Width/2
cy := p.Y + p.Height/2
b.WriteString(fmt.Sprintf(`<text class="label" x="%.2f" y="%.2f">%s</text>`+"\n", cx, cy, p.Label))
b.WriteString("</g>\n")
}
// Fold lines between lid and side panels
for _, p := range layout.Panels {
if p.FaceType == "side" {
b.WriteString(fmt.Sprintf(`<line class="fold-line" x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" />`+"\n",
p.X, p.Y, p.X+p.Width, p.Y))
}
if p.FaceType == "tray" {
b.WriteString(fmt.Sprintf(`<line class="fold-line" x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" />`+"\n",
p.X, p.Y, p.X+p.Width, p.Y))
}
}
b.WriteString("</svg>")
return b.String()
}