Former/instance.go

226 lines
6.6 KiB
Go

package main
import (
"fmt"
"image"
"log"
"os"
"path/filepath"
"strings"
"time"
)
// InstanceData holds the serializable state for a saved enclosure instance.
type InstanceData struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
CreatedAt time.Time `json:"createdAt"`
// Source files (basenames relative to the project directory)
GerberFiles map[string]string `json:"gerberFiles"`
DrillPath string `json:"drillPath,omitempty"`
NPTHPath string `json:"npthPath,omitempty"`
// Discovered layer filenames (keys into GerberFiles)
EdgeCutsFile string `json:"edgeCutsFile"`
CourtyardFile string `json:"courtyardFile,omitempty"`
SoldermaskFile string `json:"soldermaskFile,omitempty"`
FabFile string `json:"fabFile,omitempty"`
// Configuration
Config EnclosureConfig `json:"config"`
Exports []string `json:"exports"`
// Board display info
BoardW float64 `json:"boardW"`
BoardH float64 `json:"boardH"`
ProjectName string `json:"projectName,omitempty"`
// Unified cutouts (new format)
Cutouts []Cutout `json:"cutouts,omitempty"`
// Legacy cutout fields — kept for backward compatibility when loading old projects
SideCutouts []SideCutout `json:"sideCutouts,omitempty"`
LidCutouts []LidCutout `json:"lidCutouts,omitempty"`
}
// MigrateCutouts returns the unified cutouts list, converting legacy fields if needed.
func (inst *InstanceData) MigrateCutouts() []Cutout {
if len(inst.Cutouts) > 0 {
return inst.Cutouts
}
// Migrate legacy side cutouts
var result []Cutout
for _, sc := range inst.SideCutouts {
result = append(result, Cutout{
ID: randomID(),
Surface: "side",
SideNum: sc.Side,
X: sc.X,
Y: sc.Y,
Width: sc.Width,
Height: sc.Height,
CornerRadius: sc.CornerRadius,
SourceLayer: sc.Layer,
})
}
// Migrate legacy lid cutouts
for _, lc := range inst.LidCutouts {
surface := "top"
if lc.Plane == "tray" {
surface = "bottom"
}
result = append(result, Cutout{
ID: randomID(),
Surface: surface,
X: lc.MinX,
Y: lc.MinY,
Width: lc.MaxX - lc.MinX,
Height: lc.MaxY - lc.MinY,
IsDado: lc.IsDado,
Depth: lc.Depth,
})
}
return result
}
// restoreSessionFromDir rebuilds an EnclosureSession from an InstanceData,
// resolving all file paths relative to baseDir.
func restoreSessionFromDir(inst *InstanceData, baseDir string) (string, *EnclosureSession, error) {
outlineBasename, ok := inst.GerberFiles[inst.EdgeCutsFile]
if !ok {
return "", nil, fmt.Errorf("edge cuts file not found: %s", inst.EdgeCutsFile)
}
outlinePath := filepath.Join(baseDir, outlineBasename)
if _, err := os.Stat(outlinePath); err != nil {
return "", nil, fmt.Errorf("source files no longer available: %v", err)
}
outlineGf, err := ParseGerber(outlinePath)
if err != nil {
return "", nil, fmt.Errorf("parse outline: %v", err)
}
outlineBounds := outlineGf.CalculateBounds()
actualBoardW := outlineBounds.MaxX - outlineBounds.MinX
actualBoardH := outlineBounds.MaxY - outlineBounds.MinY
ecfg := inst.Config
margin := ecfg.WallThickness + ecfg.Clearance + 5.0
outlineBounds.MinX -= margin
outlineBounds.MinY -= margin
outlineBounds.MaxX += margin
outlineBounds.MaxY += margin
ecfg.OutlineBounds = &outlineBounds
outlineImg := outlineGf.Render(ecfg.DPI, &outlineBounds)
minBX, maxBX := outlineImg.Bounds().Max.X, -1
var boardCenterY float64
var boardCount int
_, boardMask := ComputeWallMask(outlineImg, ecfg.WallThickness+ecfg.Clearance, ecfg.DPI/25.4)
imgW, imgH := outlineImg.Bounds().Max.X, outlineImg.Bounds().Max.Y
for y := 0; y < imgH; y++ {
for x := 0; x < imgW; x++ {
if boardMask[y*imgW+x] {
if x < minBX {
minBX = x
}
if x > maxBX {
maxBX = x
}
boardCenterY += float64(y)
boardCount++
}
}
}
if boardCount > 0 {
boardCenterY /= float64(boardCount)
}
// Resolve gerber paths relative to baseDir and render ALL layers
resolvedGerbers := make(map[string]string)
allLayers := make(map[string]image.Image)
allGerbers := make(map[string]*GerberFile)
for origName, basename := range inst.GerberFiles {
fullPath := filepath.Join(baseDir, basename)
resolvedGerbers[origName] = fullPath
if strings.HasSuffix(strings.ToLower(origName), ".gbrjob") {
continue
}
if origName == inst.EdgeCutsFile {
allLayers[origName] = outlineImg
allGerbers[origName] = outlineGf
continue
}
if gf, err := ParseGerber(fullPath); err == nil {
allLayers[origName] = gf.Render(ecfg.DPI, &outlineBounds)
allGerbers[origName] = gf
}
}
var courtyardImg, soldermaskImg image.Image
if inst.CourtyardFile != "" {
courtyardImg = allLayers[inst.CourtyardFile]
}
if inst.SoldermaskFile != "" {
soldermaskImg = allLayers[inst.SoldermaskFile]
}
if courtyardImg == nil && inst.FabFile != "" {
courtyardImg = allLayers[inst.FabFile]
}
var drillHoles []DrillHole
if inst.DrillPath != "" {
if holes, err := ParseDrill(filepath.Join(baseDir, inst.DrillPath)); err == nil {
drillHoles = append(drillHoles, holes...)
}
}
if inst.NPTHPath != "" {
if holes, err := ParseDrill(filepath.Join(baseDir, inst.NPTHPath)); err == nil {
drillHoles = append(drillHoles, holes...)
}
}
var filteredHoles []DrillHole
for _, h := range drillHoles {
if h.Type != DrillTypeVia {
filteredHoles = append(filteredHoles, h)
}
}
pixelToMM := 25.4 / ecfg.DPI
sessionID := randomID()
session := &EnclosureSession{
Exports: inst.Exports,
OutlineGf: outlineGf,
OutlineImg: outlineImg,
CourtyardImg: courtyardImg,
SoldermaskImg: soldermaskImg,
DrillHoles: filteredHoles,
Config: ecfg,
OutlineBounds: outlineBounds,
BoardW: actualBoardW,
BoardH: actualBoardH,
TotalH: ecfg.WallHeight + ecfg.PCBThickness + 1.0,
MinBX: float64(minBX)*pixelToMM + outlineBounds.MinX,
MaxBX: float64(maxBX)*pixelToMM + outlineBounds.MinX,
BoardCenterY: outlineBounds.MaxY - boardCenterY*pixelToMM,
Sides: ExtractBoardSidesFromMask(boardMask, imgW, imgH, pixelToMM, &outlineBounds),
GerberFiles: inst.GerberFiles,
DrillPath: inst.DrillPath,
NPTHPath: inst.NPTHPath,
ProjectName: inst.ProjectName,
EdgeCutsFile: inst.EdgeCutsFile,
CourtyardFile: inst.CourtyardFile,
SoldermaskFile: inst.SoldermaskFile,
FabFile: inst.FabFile,
EnclosureWallImg: renderEnclosureWallImage(outlineImg, ecfg),
AllLayerImages: allLayers,
AllLayerGerbers: allGerbers,
SourceDir: baseDir,
}
log.Printf("Restored session %s from %s (%s)", sessionID, baseDir, inst.ProjectName)
return sessionID, session, nil
}