package main import ( "encoding/json" "fmt" "os" "strings" ) // GerberJob represents a KiCad .gbrjob file type GerberJob struct { Header struct { GenerationSoftware struct { Vendor string `json:"Vendor"` Application string `json:"Application"` Version string `json:"Version"` } `json:"GenerationSoftware"` } `json:"Header"` GeneralSpecs struct { ProjectId struct { Name string `json:"Name"` } `json:"ProjectId"` Size struct { X float64 `json:"X"` Y float64 `json:"Y"` } `json:"Size"` BoardThickness float64 `json:"BoardThickness"` } `json:"GeneralSpecs"` FilesAttributes []struct { Path string `json:"Path"` FileFunction string `json:"FileFunction"` FilePolarity string `json:"FilePolarity"` } `json:"FilesAttributes"` } // GerberJobResult contains the auto-discovered file assignments type GerberJobResult struct { ProjectName string BoardWidth float64 // mm BoardHeight float64 // mm BoardThickness float64 // mm EdgeCutsFile string // Profile FabFile string // AssemblyDrawing,Top CourtyardFile string // matches courtyard naming SoldermaskFile string // matches mask naming } // ParseGerberJob parses a .gbrjob file and returns auto-discovered file mappings func ParseGerberJob(filename string) (*GerberJobResult, error) { data, err := os.ReadFile(filename) if err != nil { return nil, fmt.Errorf("read gbrjob: %w", err) } var job GerberJob if err := json.Unmarshal(data, &job); err != nil { return nil, fmt.Errorf("parse gbrjob JSON: %w", err) } result := &GerberJobResult{ ProjectName: job.GeneralSpecs.ProjectId.Name, BoardWidth: job.GeneralSpecs.Size.X, BoardHeight: job.GeneralSpecs.Size.Y, BoardThickness: job.GeneralSpecs.BoardThickness, } // Map FileFunction to our layer types for _, f := range job.FilesAttributes { fn := strings.ToLower(f.FileFunction) path := f.Path switch { case fn == "profile": result.EdgeCutsFile = path case strings.HasPrefix(fn, "assemblydrawing"): // F.Fab = AssemblyDrawing,Top if strings.Contains(fn, "top") { result.FabFile = path } } // Also match by filename patterns for courtyard/mask lp := strings.ToLower(path) switch { case strings.Contains(lp, "courtyard") || strings.Contains(lp, "courty"): if strings.Contains(lp, "f_courty") || strings.Contains(lp, "f.courty") || strings.Contains(lp, "-f_courty") { result.CourtyardFile = path } case strings.Contains(lp, "mask") || strings.Contains(lp, "f_mask") || strings.Contains(lp, "f.mask"): if strings.Contains(lp, "f_mask") || strings.Contains(lp, "f.mask") || strings.Contains(lp, "-f_mask") { result.SoldermaskFile = path } } } fmt.Printf("GerberJob: project=%s, board=%.1fx%.1fmm, thickness=%.1fmm\n", result.ProjectName, result.BoardWidth, result.BoardHeight, result.BoardThickness) fmt.Printf(" EdgeCuts: %s\n", result.EdgeCutsFile) fmt.Printf(" F.Fab: %s\n", result.FabFile) fmt.Printf(" F.Courtyard: %s\n", result.CourtyardFile) fmt.Printf(" F.Mask: %s\n", result.SoldermaskFile) if result.EdgeCutsFile == "" { return nil, fmt.Errorf("no Edge.Cuts (Profile) layer found in gbrjob") } return result, nil }