301 lines
11 KiB
Go
301 lines
11 KiB
Go
package editor
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
|
|
"aego/art"
|
|
"aego/asset/importers/artsource"
|
|
"aego/asset/pipeline"
|
|
"aego/core/ids"
|
|
"aego/editor/core/artedit"
|
|
"aego/editor/core/assetops"
|
|
"aego/editor/core/context"
|
|
"aego/editor/core/document"
|
|
"aego/engine/i18n"
|
|
"aego/project"
|
|
)
|
|
|
|
// The draft owns no disk files. The original PNG identity continues to own
|
|
// its working model until an explicit, guarded first Save links a new source.
|
|
type pngArtDraft struct {
|
|
ed *context.Editor
|
|
doc *document.AssetDocument
|
|
docs *document.Manager
|
|
pipe *pipeline.Pipeline
|
|
project *project.Project
|
|
path string
|
|
meta *pipeline.Meta
|
|
metadata *document.ScriptDocument
|
|
metaRev uint64
|
|
guards map[string][]byte
|
|
frames []artsource.TextureLinkFrame
|
|
model *artedit.Document
|
|
}
|
|
|
|
func openPNGArtDraft(ed *context.Editor, doc *document.AssetDocument) (*pngArtDraft, *artsource.TextureLink, error) {
|
|
if ed == nil || ed.Pipeline == nil || ed.Docs == nil || doc == nil || !doc.Capabilities.Has(document.EditSource) {
|
|
return nil, nil, fmt.Errorf("art: this PNG source is not writable")
|
|
}
|
|
if err := ed.Pipeline.CheckSourceRecovery(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if doc.Binding.Model != nil {
|
|
return nil, nil, fmt.Errorf("art: this image already has another open structured editor")
|
|
}
|
|
if doc.Metadata == nil {
|
|
return nil, nil, fmt.Errorf("%s", i18n.Tr("editor.art.metadata_required", "Repair the image metadata before creating an editable source."))
|
|
}
|
|
if doc.Metadata.ReadOnly || doc.Metadata.Dirty() || doc.Metadata.Conflict {
|
|
return nil, nil, fmt.Errorf("%s", i18n.Tr("editor.art.save_image_settings", "Save or resolve the image import settings before creating a linked art source."))
|
|
}
|
|
d := &pngArtDraft{ed: ed, doc: doc, docs: ed.Docs, pipe: ed.Pipeline, project: ed.Project, path: doc.Path,
|
|
metadata: doc.Metadata, metaRev: doc.Metadata.Code.Rev(), guards: make(map[string][]byte)}
|
|
read := func(name string) ([]byte, error) {
|
|
if script := d.docs.Script(name); script != nil && (script.Dirty() || script.Conflict) {
|
|
return nil, fmt.Errorf("%s", i18n.Tr("editor.art.resolve_source_settings", "Save or resolve unsaved source settings before linking: {path}", i18n.Str("path", name)))
|
|
}
|
|
if data, ok := d.guards[name]; ok {
|
|
return data, nil
|
|
}
|
|
data, err := d.pipe.SourceBytes(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if data == nil {
|
|
return nil, fmt.Errorf("art: source no longer exists: %s", name)
|
|
}
|
|
d.guards[name] = bytes.Clone(data)
|
|
return d.guards[name], nil
|
|
}
|
|
metaBytes, err := read(d.path + pipeline.MetaSuffix)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
normalized := bytes.ReplaceAll(bytes.ReplaceAll(metaBytes, []byte("\r\n"), []byte("\n")), []byte("\r"), []byte("\n"))
|
|
if !bytes.Equal(normalized, d.metadata.Code.Bytes()) {
|
|
return nil, nil, fmt.Errorf("%s", i18n.Tr("editor.art.external_source_conflict", "The image source or its metadata changed outside the editor. Resolve the conflict before linking."))
|
|
}
|
|
d.meta, err = pipeline.ParseMeta(metaBytes, d.path+pipeline.MetaSuffix)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
link, err := artsource.TextureLinkOf(d.meta)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
png, err := read(d.path)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if link != nil {
|
|
hash := sha256.Sum256(png)
|
|
if hex.EncodeToString(hash[:]) != link.SHA256 {
|
|
return nil, nil, fmt.Errorf("%s", i18n.Tr("editor.art.external_source_conflict", "The image source or its metadata changed outside the editor. Resolve the conflict before linking."))
|
|
}
|
|
if err := d.verify(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return nil, link, nil
|
|
}
|
|
pixels, frames, err := artsource.ImportTextureDocument(&pipeline.ImportContext{
|
|
ID: doc.Asset, Path: d.path, Source: png, Meta: d.meta,
|
|
Read: read, SourcePath: d.pipe.PathOf, Lookup: d.pipe.Lookup,
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
d.frames = frames
|
|
hash := sha256.New()
|
|
fmt.Fprintln(hash, "aego-png-art-recovery-v1")
|
|
paths := make([]string, 0, len(d.guards))
|
|
for name := range d.guards {
|
|
paths = append(paths, name)
|
|
}
|
|
slices.Sort(paths)
|
|
for _, name := range paths {
|
|
data := d.guards[name]
|
|
fmt.Fprintf(hash, "%d:%s:%d:", len(name), name, len(data))
|
|
_, _ = hash.Write(data)
|
|
}
|
|
for _, frame := range frames {
|
|
fmt.Fprintf(hash, "\n%s:%s", frame.Asset.String(), frame.Frame)
|
|
}
|
|
d.model, err = artedit.BindImported(d.docs, doc, pixels, png, hex.EncodeToString(hash.Sum(nil)), d.verify)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return d, nil, nil
|
|
}
|
|
|
|
func (d *pngArtDraft) verify() error {
|
|
if err := d.pipe.CheckSourceRecovery(); err != nil {
|
|
return err
|
|
}
|
|
if d.ed.Docs != d.docs || d.ed.Pipeline != d.pipe || d.ed.Project != d.project || d.docs.AssetDocument(d.doc.Asset) != d.doc || d.doc.Path != d.path || d.doc.Metadata != d.metadata || d.metadata.Code.Rev() != d.metaRev || d.metadata.ReadOnly || d.metadata.Dirty() || d.metadata.Conflict {
|
|
return fmt.Errorf("%s", i18n.Tr("editor.art.link_target_changed", "The project, image or import settings changed before linking. Open Edit Source Art again."))
|
|
}
|
|
if name, ok := d.pipe.PathOf(d.doc.Asset); !ok || name != d.path {
|
|
return fmt.Errorf("art: image identity or path changed")
|
|
}
|
|
for name, before := range d.guards {
|
|
if source := d.docs.Script(name); source != nil && (source.Dirty() || source.Conflict) {
|
|
return fmt.Errorf("%s", i18n.Tr("editor.art.resolve_source_settings", "Save or resolve unsaved source settings before linking: {path}", i18n.Str("path", name)))
|
|
}
|
|
current, err := d.pipe.SourceBytes(name)
|
|
if err != nil || current == nil || !bytes.Equal(before, current) {
|
|
return fmt.Errorf("%s", i18n.Tr("editor.art.external_source_conflict", "The image source or its metadata changed outside the editor. Resolve the conflict before linking."))
|
|
}
|
|
}
|
|
return d.pipe.CheckSourceRecovery()
|
|
}
|
|
|
|
func (d *pngArtDraft) selectFrame(id ids.AssetID) error {
|
|
if d.model == nil || len(d.frames) == 0 {
|
|
return fmt.Errorf("art: image has no editable frame")
|
|
}
|
|
frame := d.frames[0].Frame
|
|
found := id == d.doc.Asset
|
|
for _, candidate := range d.frames {
|
|
if candidate.Asset == id {
|
|
frame = candidate.Frame
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("%s", i18n.Tr("editor.art.frame_unavailable", "The selected art frame is no longer available."))
|
|
}
|
|
if d.model.Session.Frame == frame && d.model.Session.Channel == art.Albedo {
|
|
return nil
|
|
}
|
|
return d.model.Session.Select("", frame, art.Albedo)
|
|
}
|
|
|
|
func (d *pngArtDraft) askSave(panel *artPanel, transferred func(*document.AssetDocument) error) error {
|
|
if err := d.verify(); err != nil {
|
|
return err
|
|
}
|
|
if d.model.PendingRecovery || d.model.Session.Info().Gesture {
|
|
return fmt.Errorf("art: resolve recovery and finish the current gesture before saving")
|
|
}
|
|
session := d.model.Session
|
|
snapshot, err := session.Document.Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
encoded, err := snapshot.Encode()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
revision := d.model.PreviewRevision()
|
|
current := func() bool {
|
|
return d.doc.Binding.Model == d.model && d.model.Draft && !d.model.PendingRecovery && d.model.Session == session && !session.Info().Gesture && d.model.PreviewCurrent(revision)
|
|
}
|
|
stem := strings.TrimSuffix(path.Base(d.path), path.Ext(d.path))
|
|
d.ed.Ask(context.Question{
|
|
Title: i18n.Tr("editor.art.save_png_draft", "Save editable image source"),
|
|
Message: i18n.Tr("editor.art.save_png_draft_notice", "Save the drawing as an .aeart source and link it in the PNG import metadata. Replacing an existing drawing requires confirmation. The PNG itself is not overwritten; changes affect all users of this shared image."),
|
|
Fields: []string{i18n.Tr("editor.art.source_path", "Source path")},
|
|
Values: []string{assetops.UniqueFile(d.ed, path.Dir(d.path), stem, art.SourceExtension)},
|
|
Buttons: []string{i18n.Tr("editor.art.save", "Save"), i18n.Tr("editor.art.cancel", "Cancel")},
|
|
DestructiveLabel: i18n.Tr("editor.art.replace", "Replace"),
|
|
DestructiveWhen: func(values []string) bool { return artSaveReplaces(d.ed, values) },
|
|
Submit: func(values []string) {
|
|
fail := func(err error) { panel.report(err) }
|
|
if len(values) != 1 || !current() {
|
|
fail(art.ErrConflict)
|
|
return
|
|
}
|
|
if err := d.verify(); err != nil {
|
|
fail(err)
|
|
return
|
|
}
|
|
target, err := prepareArtSaveTarget(d.ed, d.doc, strings.TrimSpace(values[0]), snapshot)
|
|
if err != nil {
|
|
fail(err)
|
|
return
|
|
}
|
|
if !target.replacement {
|
|
for _, key := range []string{"filter", "colorspace", "srgb"} {
|
|
if value, ok := d.meta.Settings[key]; ok {
|
|
target.meta.Settings[key] = value
|
|
}
|
|
}
|
|
}
|
|
target.confirm(func() {
|
|
if !current() {
|
|
fail(art.ErrConflict)
|
|
return
|
|
}
|
|
if err := d.verify(); err != nil {
|
|
fail(err)
|
|
return
|
|
}
|
|
if err := target.verify(); err != nil {
|
|
fail(err)
|
|
return
|
|
}
|
|
metaBefore := d.guards[d.path+pipeline.MetaSuffix]
|
|
linked, err := pipeline.ParseMeta(metaBefore, d.path+pipeline.MetaSuffix)
|
|
if err == nil {
|
|
err = artsource.SetTextureLink(linked, target.meta.ID, d.guards[d.path], d.frames)
|
|
}
|
|
if err != nil {
|
|
fail(err)
|
|
return
|
|
}
|
|
edits := target.edits(encoded)
|
|
edits = append(edits, pipeline.SourceEdit{Path: d.path + pipeline.MetaSuffix, Before: metaBefore, After: pipeline.WriteMeta(linked)})
|
|
paths := make([]string, 0, len(d.guards))
|
|
for name := range d.guards {
|
|
if name != d.path+pipeline.MetaSuffix {
|
|
paths = append(paths, name)
|
|
}
|
|
}
|
|
slices.Sort(paths)
|
|
for _, name := range paths {
|
|
edits = append(edits, pipeline.SourceEdit{Path: name, Before: d.guards[name], After: d.guards[name], ReadOnlyGuard: true})
|
|
}
|
|
var batch *pipeline.Batch
|
|
err = session.Document.Publish(snapshot, func() error {
|
|
var err error
|
|
batch, err = d.pipe.ApplySources(edits, false)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
fail(err)
|
|
return
|
|
}
|
|
d.ed.ApplyBatch(batch)
|
|
if err := d.docs.AcceptSavedScript(d.metadata, metaBefore, edits[2].After); err != nil {
|
|
fail(fmt.Errorf("art: source was published, but the open image settings could not be synchronized; the drawing is still open: %w", err))
|
|
return
|
|
}
|
|
next, err := d.ed.DocumentForAsset(target.meta.ID)
|
|
if err == nil {
|
|
err = d.model.RebindSaved(next, snapshot)
|
|
}
|
|
if err != nil {
|
|
fail(fmt.Errorf("art: source was published, but the drawing remains open as an unsaved draft: %w", err))
|
|
return
|
|
}
|
|
if err := d.docs.ClearOwnedAssetRecovery(d.doc); err != nil {
|
|
d.ed.Notify(context.LevelWarn, err.Error())
|
|
}
|
|
if err := transferred(next); err != nil {
|
|
fail(err)
|
|
return
|
|
}
|
|
d.ed.Notify(context.LevelInfo, i18n.Tr("editor.art.png_draft_saved", "Saved linked drawing: {path}", i18n.Str("path", target.path)))
|
|
})
|
|
},
|
|
})
|
|
return document.ErrSavePending
|
|
}
|