227 lines
6.7 KiB
Go
227 lines
6.7 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,
|
|
Shape: lc.Shape,
|
|
})
|
|
}
|
|
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
|
|
}
|