Files
sanspie a46347e2c6
ci-platforms / darwin arm64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m19s
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
ci / checks (push) Failing after 23m57s
ci / release (push) Skipped
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: failure
GitHub / darwin amd64 native GitHub-hosted native CI: failure
GitHub / linux amd64 native GitHub-hosted native CI: success
editor: add art studio and stage 12.5 workflows
2026-11-08 18:47:57 +03:00

196 lines
6.3 KiB
Go

package agentbridge
import (
"bytes"
"context"
"fmt"
"image/png"
"path"
"strconv"
"strings"
"aego/app/agent"
"aego/art"
"aego/editor/core/artedit"
"aego/scene/mutate"
)
var _ agent.ArtHost = (*Bridge)(nil)
func (b *Bridge) artDocument(file string) (*artedit.Document, error) {
rel, err := b.Rel(file)
if err != nil {
return nil, err
}
if !strings.EqualFold(path.Ext(rel), ".aeart") {
return nil, fmt.Errorf("art: an .aeart source is required")
}
if b.ctx.Docs == nil || b.ctx.Pipeline == nil {
return nil, fmt.Errorf("art: project documents are unavailable")
}
id, ok := b.ctx.Pipeline.Lookup(rel)
if !ok {
return nil, fmt.Errorf("art: %s is not in the project asset index", rel)
}
d, err := b.ctx.DocumentForAsset(id)
if err != nil {
return nil, err
}
return artedit.Bind(b.ctx.Docs, d)
}
// All ArtHost methods are reached through Bridge's existing main-thread queue.
func (b *Bridge) ReadArt(file, section string, offset, limit int) (*agent.Output, error) {
d, err := b.artDocument(file)
if err != nil {
return nil, err
}
i := d.Session.Info()
value := map[string]any{
"file": d.Asset.Path, "documentID": i.Document.ID, "revision": strconv.FormatUint(i.Document.Revision, 10),
"canvas": i.Document.Canvas, "dirty": i.Dirty, "gesture": i.Gesture,
"activeLayer": i.Layer, "activeFrame": i.Frame, "activeChannel": i.Channel,
"undo": i.Undo, "redo": i.Redo,
"counts": map[string]int{"layers": len(i.Document.Layers), "frames": len(i.Document.Frames), "cels": len(i.Document.Cels), "palette": len(i.Document.Palette), "tags": len(i.Document.Tags), "slices": len(i.Document.Slices)},
}
if offset < 0 || limit < 1 || limit > 1024 {
return nil, fmt.Errorf("art: invalid page")
}
var total, next int
var items any
switch section {
case "", "summary":
return &agent.Output{Value: value}, nil
case "layers":
items, total, next = artPage(i.Document.Layers, offset, limit)
case "frames":
items, total, next = artPage(i.Document.Frames, offset, limit)
case "cels":
items, total, next = artPage(i.Document.Cels, offset, limit)
case "palette":
items, total, next = artPage(i.Document.Palette, offset, limit)
case "tags":
items, total, next = artPage(i.Document.Tags, offset, limit)
case "slices":
items, total, next = artPage(i.Document.Slices, offset, limit)
default:
return nil, fmt.Errorf("art: unknown section %q", section)
}
value["section"], value["items"], value["total"], value["nextOffset"] = section, items, total, next
return &agent.Output{Value: value}, nil
}
func artPage[T any](values []T, offset, limit int) ([]T, int, int) {
start := min(offset, len(values))
end := min(start+limit, len(values))
next := -1
if end < len(values) {
next = end
}
return values[start:end], len(values), next
}
func (b *Bridge) CaptureArt(file string, frame art.ID, channel art.Channel) (*agent.Output, error) {
d, err := b.artDocument(file)
if err != nil {
return nil, err
}
if frame == "" {
frame = d.Session.Frame
}
if channel == "" {
channel = d.Session.Channel
}
pixels, revision, err := d.Capture(frame, channel)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err = png.Encode(&buf, pixels); err != nil {
return nil, err
}
if buf.Len() > 16<<20 {
return nil, fmt.Errorf("art: PNG exceeds agent capture budget")
}
return &agent.Output{
Value: map[string]any{"file": d.Asset.Path, "documentID": d.Session.Document.Info().ID, "revision": strconv.FormatUint(revision, 10), "frame": frame, "channel": channel, "width": pixels.Bounds().Dx(), "height": pixels.Bounds().Dy(), "dirty": d.Session.Info().Dirty},
Images: []agent.Image{{Mime: "image/png", Data: buf.Bytes()}},
}, nil
}
type preparedArt struct {
bridge *Bridge
owner *artedit.Document
proposal *artedit.Proposal
label string
used bool
}
func (p *preparedArt) RetainedBytes() int64 { return p.proposal.Bytes() }
func (p *preparedArt) Commit() (*agent.Output, error) {
if p.used {
return nil, fmt.Errorf("art: proposal has already been consumed")
}
p.used = true
b, d := p.bridge, p.owner
if b.PlanOnly() {
return nil, errPlanOnly
}
if b.ctx.Docs.AssetDocument(d.Asset.Asset) != d.Asset || d.Asset.Binding.Model != d {
return nil, fmt.Errorf("art: document was closed or replaced while awaiting approval")
}
if err := d.Accept(p.proposal); err != nil {
return nil, err
}
b.last = &undoRef{doc: d.Asset.ID, label: p.label, art: &artUndo{owner: d, revision: d.Session.Document.Revision()}}
return &agent.Output{OK: true, Value: map[string]any{
"file": d.Asset.Path, "documentID": d.Session.Document.Info().ID, "revision": strconv.FormatUint(d.Session.Document.Revision(), 10), "results": p.proposal.Results, "dirty": d.Session.Info().Dirty,
}}, nil
}
func (b *Bridge) PrepareArt(file string, revision uint64, label string, ops []art.Operation) (*agent.Output, error) {
if b.PlanOnly() {
return nil, errPlanOnly
}
d, err := b.artDocument(file)
if err != nil {
return nil, err
}
label = b.stepName(label)
p, err := d.Propose(context.Background(), revision, label, ops)
if err != nil {
return nil, err
}
var held int64
for _, pending := range b.proposals {
if pending.Preview != nil && pending.Preview.Art != nil {
held += pending.Preview.Art.RetainedBytes()
}
}
if p.Bytes() > (64<<20)-held {
return nil, fmt.Errorf("art: pending proposals exceed memory budget; resolve existing proposals first")
}
return &agent.Output{
DryRun: true,
Changes: []mutate.Change{{Op: "art.edit", Node: d.Asset.Path, Path: "pixels/layers/frames", Before: strconv.FormatUint(revision, 10), After: fmt.Sprintf("%d operations: %s", len(ops), label)}},
Value: map[string]any{"file": d.Asset.Path, "documentID": d.Session.Document.Info().ID, "revision": strconv.FormatUint(revision, 10), "results": p.Results},
Art: &preparedArt{bridge: b, owner: d, proposal: p, label: label},
}, nil
}
type artUndo struct {
owner *artedit.Document
revision uint64
}
func (b *Bridge) canUndoArt(u *artUndo) bool {
if u == nil || u.owner == nil || b.ctx.Docs == nil {
return false
}
d := u.owner
return b.ctx.Docs.AssetDocument(d.Asset.Asset) == d.Asset && d.Asset.Binding.Model == d && !d.Session.Info().Gesture && d.Session.Document.Revision() == u.revision && d.History.CanUndo()
}
func (b *Bridge) undoArt(u *artUndo) error {
if !b.canUndoArt(u) {
return fmt.Errorf("art: proposal is no longer the latest document edit")
}
return u.owner.SessionUndo()
}