automated snapshot

This commit is contained in:
sumi
2025-12-24 11:38:45 -06:00
parent 87c8a97e2a
commit 3baa135b47
3 changed files with 171 additions and 35 deletions

View File

@@ -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
}

View File

@@ -32,6 +32,7 @@ type LayerTools struct {
name string name string
layer Layer layer Layer
texture rl.RenderTexture2D texture rl.RenderTexture2D
capture *rl.Image
config *LayerConfig config *LayerConfig
} }
@@ -241,21 +242,24 @@ func (s *Sketch) ResetCamera() {
} }
type SketchCapture struct { type SketchCapture struct {
width, height uint32
compositeImage *rl.Image compositeImage *rl.Image
layerImages []*rl.Image layerTools map[string]*LayerTools
layerToolsOrdered []*LayerTools
} }
func (s *Sketch) Capture() *SketchCapture { func (s *Sketch) Capture() *SketchCapture {
composite := rl.LoadImageFromTexture(s.composite.Texture) composite := rl.LoadImageFromTexture(s.composite.Texture)
rl.ImageFlipVertical(composite) rl.ImageFlipVertical(composite)
layerImages := make([]*rl.Image, len(s.layerToolsOrdered)) for _, layerTool := range s.layerToolsOrdered {
for i, layerTool := range s.layerToolsOrdered { layerTool.capture = rl.LoadImageFromTexture(layerTool.texture.Texture)
layerImages[i] = rl.LoadImageFromTexture(layerTool.texture.Texture) rl.ImageFlipVertical(layerTool.capture)
rl.ImageFlipVertical(layerImages[i])
} }
return &SketchCapture { return &SketchCapture {
width: uint32(s.sourceWidth), height: uint32(s.sourceHeight),
compositeImage: composite, compositeImage: composite,
layerImages: layerImages, layerTools: s.layerTools,
layerToolsOrdered: s.layerToolsOrdered,
} }
} }

View File

@@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/d2fn/sumi/internal/ids" "github.com/d2fn/sumi/internal/ids"
"github.com/d2fn/sumi/internal/ora"
"github.com/go-git/go-git/v6" "github.com/go-git/go-git/v6"
"log" "log"
//"github.com/go-git/go-git/v5/plumbing" //"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) { func (s *Storage) Save(capture *SketchCapture) (string, error) {
id, _ := s.gen.Next() id, _ := s.gen.Next()
kid := ids.Base62Encode(id) flakeId := ids.Base62Encode(id)
path := filepath.Join(s.snapshotsDir, kid) path := filepath.Join(s.snapshotsDir, flakeId)
os.MkdirAll(path, 0755) os.MkdirAll(path, 0755)
// wysiwyg at screen res hash, branch, committed, err := s.SaveToGit(flakeId)
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)
if err != nil { if err != nil {
s.log.Printf("Error getting working tree in a known clean state: %v", err) s.log.Printf("Error getting working tree in a known clean state: %v", err)
} else { } else {
if committed { 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 { } 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) INSERT INTO snapshots (id, sid, created_at, branch, git_hash, committed, path)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
`, `,
id, id,
kid, flakeId,
time.Now().UnixMilli(), time.Now().UnixMilli(),
branch, branch,
hash, 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("Error inserting snapshot row into db: %v\n", err)
} }
s.log.Printf("Saved snapshot to %s\n", path) return err
return path, nil
} }
func HeadHash(repoPath string) (string, error) { func HeadHash(repoPath string) (string, error) {