388 lines
9.6 KiB
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()
|
|
}
|