From 3baa135b472cbed2545fa6bef66b668d4e616f8f Mon Sep 17 00:00:00 2001 From: sumi Date: Wed, 24 Dec 2025 11:38:45 -0600 Subject: [PATCH] automated snapshot --- internal/ora/ora_support.go | 98 +++++++++++++++++++++++++++++++++++++ sketch.go | 16 +++--- storage.go | 92 +++++++++++++++++++++++----------- 3 files changed, 171 insertions(+), 35 deletions(-) create mode 100644 internal/ora/ora_support.go diff --git a/internal/ora/ora_support.go b/internal/ora/ora_support.go new file mode 100644 index 0000000..0ca8dfe --- /dev/null +++ b/internal/ora/ora_support.go @@ -0,0 +1,98 @@ +package ora + +import ( + "archive/zip" + "encoding/xml" + "os" +) + +type ORALayer struct { + Name string + Filename string // relative to data/ + Visible bool + Opacity float32 // 0..1 + Blend string // svg:src-over, svg:multiply, etc +} + +type imageXML struct { + XMLName xml.Name `xml:"image"` + W int `xml:"w,attr"` + H int `xml:"h,attr"` + Stack stackXML `xml:"stack"` +} + +type stackXML struct { + Layers []layerXML `xml:"layer"` +} + +type layerXML struct { + Name string `xml:"name,attr"` + Src string `xml:"src,attr"` + Opacity float32 `xml:"opacity,attr"` + Visible bool `xml:"visible,attr"` + Composite string `xml:"composite-op,attr"` +} + +func WriteORA( + outPath string, + width, height int, + layers []ORALayer, + pngLoader func(name string) ([]byte, error), +) error { + + f, err := os.Create(outPath) + if err != nil { + return err + } + defer f.Close() + + zw := zip.NewWriter(f) + defer zw.Close() + + // 1. mimetype (must be first, uncompressed) + h := &zip.FileHeader{ + Name: "mimetype", + Method: zip.Store, + } + w, _ := zw.CreateHeader(h) + w.Write([]byte("image/openraster")) + + // 2. stack.xml + var stack []layerXML + for _, l := range layers { + stack = append(stack, layerXML{ + Name: l.Name, + Src: "data/" + l.Filename, + Opacity: l.Opacity, + Visible: l.Visible, + Composite: l.Blend, + }) + } + + img := imageXML{ + W: width, + H: height, + Stack: stackXML{ + Layers: stack, + }, + } + + xmlBytes, _ := xml.MarshalIndent(img, "", " ") + xmlBuf := append([]byte(xml.Header), xmlBytes...) + + w, _ = zw.Create("stack.xml") + w.Write(xmlBuf) + + // 3. layer PNGs + for _, l := range layers { + data, err := pngLoader(l.Filename) + if err != nil { + return err + } + w, _ = zw.Create("data/" + l.Filename) + w.Write(data) + } + + return nil +} + diff --git a/sketch.go b/sketch.go index a0bf68a..b76e03d 100644 --- a/sketch.go +++ b/sketch.go @@ -32,6 +32,7 @@ type LayerTools struct { name string layer Layer texture rl.RenderTexture2D + capture *rl.Image config *LayerConfig } @@ -241,21 +242,24 @@ func (s *Sketch) ResetCamera() { } type SketchCapture struct { + width, height uint32 compositeImage *rl.Image - layerImages []*rl.Image + layerTools map[string]*LayerTools + layerToolsOrdered []*LayerTools } func (s *Sketch) Capture() *SketchCapture { composite := rl.LoadImageFromTexture(s.composite.Texture) rl.ImageFlipVertical(composite) - layerImages := make([]*rl.Image, len(s.layerToolsOrdered)) - for i, layerTool := range s.layerToolsOrdered { - layerImages[i] = rl.LoadImageFromTexture(layerTool.texture.Texture) - rl.ImageFlipVertical(layerImages[i]) + for _, layerTool := range s.layerToolsOrdered { + layerTool.capture = rl.LoadImageFromTexture(layerTool.texture.Texture) + rl.ImageFlipVertical(layerTool.capture) } return &SketchCapture { + width: uint32(s.sourceWidth), height: uint32(s.sourceHeight), compositeImage: composite, - layerImages: layerImages, + layerTools: s.layerTools, + layerToolsOrdered: s.layerToolsOrdered, } } diff --git a/storage.go b/storage.go index 0a7537a..e6ae4b1 100644 --- a/storage.go +++ b/storage.go @@ -3,6 +3,7 @@ package main import ( "fmt" "github.com/d2fn/sumi/internal/ids" + "github.com/d2fn/sumi/internal/ora" "github.com/go-git/go-git/v6" "log" //"github.com/go-git/go-git/v5/plumbing" @@ -86,48 +87,83 @@ func initSchema(db *sql.DB) error { } func (s *Storage) Save(capture *SketchCapture) (string, error) { + id, _ := s.gen.Next() - kid := ids.Base62Encode(id) - path := filepath.Join(s.snapshotsDir, kid) + flakeId := ids.Base62Encode(id) + path := filepath.Join(s.snapshotsDir, flakeId) os.MkdirAll(path, 0755) - // wysiwyg at screen res - img := rl.LoadImageFromScreen() - defer rl.UnloadImage(img) - - snapshotPng := filepath.Join(path, fmt.Sprintf("%s.png", kid)) - rl.ExportImage(*img, snapshotPng) - - // capture full res compsite - compositePng := filepath.Join(path, fmt.Sprintf("%s-composite.png", kid)) - rl.ExportImage(*capture.compositeImage, compositePng) - - // capture full res layer - for i, layerImage := range capture.layerImages { - layerPng := filepath.Join(path, fmt.Sprintf("%s-%03d.png", kid, i)) - rl.ExportImage(*layerImage, layerPng) - } - - s.log.Printf("Checking git status...\n") - - hash, branch, committed, err := CommitAllIfDirty(s.repoRoot, "automated snapshot", s.log) + hash, branch, committed, err := s.SaveToGit(flakeId) if err != nil { s.log.Printf("Error getting working tree in a known clean state: %v", err) } else { if committed { - s.log.Printf("Created commit %s on %s for snapshot %s", hash, branch, kid) + s.log.Printf("Created commit %s on %s for snapshot %s", hash, branch, flakeId) } else { - s.log.Printf("Referencing commit %s on %s for snapshot %s", hash, branch, kid) + s.log.Printf("Referencing commit %s on %s for snapshot %s", hash, branch, flakeId) } } - _, err = s.db.Exec(` + err = s.SaveToDb(id, flakeId, branch, hash, path, committed) + if err != nil { + s.log.Printf("Error writing to db: %v\n", err) + } + + // wysiwyg at screen res + img := rl.LoadImageFromScreen() + defer rl.UnloadImage(img) + + snapshotPng := filepath.Join(path, fmt.Sprintf("%s-screen.png", flakeId)) + rl.ExportImage(*img, snapshotPng) + + // capture full res compsite + compositePng := filepath.Join(path, fmt.Sprintf("%s-final.png", flakeId)) + rl.ExportImage(*capture.compositeImage, compositePng) + + // capture full res layer + oraLayers := make([]ora.ORALayer, len(capture.layerToolsOrdered)) + for i, layerTools := range capture.layerToolsOrdered { + filename := fmt.Sprintf("%s-%03d.png", flakeId, i) + layerPng := filepath.Join(path, "data", filename) + rl.ExportImage(*layerTools.capture, layerPng) + opacity := float32(layerTools.config.a) / 255.0 + oraLayers[i] = + ora.ORALayer{ + Name: layerTools.name, + Filename: filename, + Visible: layerTools.config.visible, + Opacity: opacity, + Blend: "svg:src-over", + } + + } + + oraPath := filepath.Join(path, fmt.Sprintf("%s-layers.ora", flakeId)) + + ora.WriteORA(oraPath, int(capture.width), int(capture.height), oraLayers, + func(name string) ([]byte, error) { + return os.ReadFile(filepath.Join(path, "data", name)) + }) + + s.log.Printf("Saved snapshot to %s\n", path) + + return path, nil +} + +func (s *Storage) SaveToGit(flakeId string) (string, string, bool, error) { + s.log.Printf("Checking git status...\n") + return CommitAllIfDirty(s.repoRoot, "automated snapshot", s.log) +} + +func (s *Storage) SaveToDb(id uint64, flakeId string, branch string, hash string, path string, committed bool) error { + + _, err := s.db.Exec(` INSERT INTO snapshots (id, sid, created_at, branch, git_hash, committed, path) VALUES (?, ?, ?, ?, ?, ?, ?) `, id, - kid, + flakeId, time.Now().UnixMilli(), branch, hash, @@ -139,9 +175,7 @@ func (s *Storage) Save(capture *SketchCapture) (string, error) { s.log.Printf("Error inserting snapshot row into db: %v\n", err) } - s.log.Printf("Saved snapshot to %s\n", path) - - return path, nil + return err } func HeadHash(repoPath string) (string, error) {