246 lines
8.4 KiB
Go
246 lines
8.4 KiB
Go
package editor
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
|
|
"aego/art"
|
|
"aego/asset"
|
|
"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"
|
|
)
|
|
|
|
func canEditSourceArt(ed *context.Editor, id ids.AssetID) bool {
|
|
if ed == nil || ed.Pipeline == nil || ed.Docs == nil || asset.IsBuiltin(id) {
|
|
return false
|
|
}
|
|
source, ok := ed.SourcePathForAsset(id)
|
|
if !ok {
|
|
return false
|
|
}
|
|
switch strings.ToLower(path.Ext(source)) {
|
|
case art.SourceExtension:
|
|
return true
|
|
case ".png":
|
|
root, ok := ed.Pipeline.Lookup(source)
|
|
if !ok {
|
|
return false
|
|
}
|
|
meta, ok := ed.Pipeline.MetaOf(root)
|
|
if !ok {
|
|
return false
|
|
}
|
|
role, err := asset.TextureRoleOf(source, meta.Settings)
|
|
return err == nil && role == asset.RoleColor
|
|
}
|
|
return false
|
|
}
|
|
|
|
// The ordinary source action uses the same canonical canvas route as Assets.
|
|
func editSourceArt(ed *context.Editor, id ids.AssetID) error {
|
|
if !canEditSourceArt(ed, id) {
|
|
return fmt.Errorf("%s", i18n.Tr("editor.art.source_unavailable", "This asset has no editable project image source."))
|
|
}
|
|
_, err := ed.OpenAsset(id)
|
|
return err
|
|
}
|
|
|
|
func openLinkedArtFrame(ed *context.Editor, link *artsource.TextureLink, selected ids.AssetID) error {
|
|
legacyNotice := func(err error) error {
|
|
if err == nil && link.LayoutSHA256 == "" {
|
|
ed.Notify(context.LevelWarn, i18n.Tr("editor.art.legacy_link_geometry", "This older PNG link cannot detect slicing changes. Keep its source rectangles unchanged and review them before importing."))
|
|
}
|
|
return err
|
|
}
|
|
var frame art.ID
|
|
for _, f := range link.Frames {
|
|
if f.Asset == selected {
|
|
frame = f.Frame
|
|
break
|
|
}
|
|
}
|
|
if frame == "" && len(link.Frames) != 0 {
|
|
frame = link.Frames[0].Frame
|
|
}
|
|
name := artsource.FrameName(frame, art.Albedo)
|
|
if source, ok := ed.Pipeline.PathOf(link.Source); ok {
|
|
if id, ok := ed.Pipeline.Lookup(source + pipeline.SubSep + name); ok {
|
|
_, err := ed.OpenAsset(id)
|
|
return legacyNotice(err)
|
|
}
|
|
}
|
|
// Reuse the canonical dirty document rather than reopening a second copy.
|
|
if existing := ed.Docs.AssetDocument(link.Source); existing != nil {
|
|
if model, ok := existing.Binding.Model.(*artedit.Document); ok {
|
|
if err := model.Session.Select("", frame, art.Albedo); err != nil {
|
|
return err
|
|
}
|
|
_, err := ed.OpenAsset(link.Source)
|
|
return legacyNotice(err)
|
|
}
|
|
}
|
|
return fmt.Errorf("%s", i18n.Tr("editor.art.linked_frame_unavailable", "The linked art frame is unavailable. Resolve its import error before editing."))
|
|
}
|
|
|
|
func askLinkedArtSource(ed *context.Editor, doc *document.AssetDocument, meta *pipeline.Meta) error {
|
|
pl, docs, project := ed.Pipeline, ed.Docs, ed.Project
|
|
selected, sourcePath := doc.Selected, doc.Path
|
|
metadata := doc.Metadata
|
|
metadataRevision := metadata.Code.Rev()
|
|
guards := map[string][]byte{}
|
|
read := func(name string) ([]byte, error) {
|
|
if source := docs.Script(name); source != nil && (source.Dirty() || source.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 := guards[name]; ok {
|
|
return data, nil
|
|
}
|
|
data, err := pl.SourceBytes(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if data == nil {
|
|
return nil, fmt.Errorf("art: source no longer exists: %s", name)
|
|
}
|
|
guards[name] = bytes.Clone(data)
|
|
return data, nil
|
|
}
|
|
before, err := read(sourcePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
metaBefore, err := read(sourcePath + pipeline.MetaSuffix)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
normalized := bytes.ReplaceAll(bytes.ReplaceAll(metaBefore, []byte("\r\n"), []byte("\n")), []byte("\r"), []byte("\n"))
|
|
if !bytes.Equal(normalized, metadata.Code.Bytes()) {
|
|
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."))
|
|
}
|
|
drawing, frames, err := artsource.ImportTextureDocument(&pipeline.ImportContext{
|
|
ID: doc.Asset, Path: sourcePath, Source: before, Meta: meta,
|
|
Read: read, SourcePath: pl.PathOf, Lookup: pl.Lookup,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer drawing.Close()
|
|
encoded, err := drawing.Encode()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stem := strings.TrimSuffix(path.Base(sourcePath), path.Ext(sourcePath))
|
|
current := func() bool {
|
|
return ed.Pipeline == pl && ed.Docs == docs && ed.Project == project && docs.AssetDocument(doc.Asset) == doc &&
|
|
doc.Path == sourcePath && doc.Metadata == metadata && metadata.Code.Rev() == metadataRevision &&
|
|
!metadata.ReadOnly && !metadata.Dirty() && !metadata.Conflict
|
|
}
|
|
submit := func(values []string) {
|
|
if len(values) != 1 {
|
|
return
|
|
}
|
|
fail := func(err error) { ed.Notify(context.LevelError, err.Error()) }
|
|
if !current() {
|
|
fail(fmt.Errorf("%s", i18n.Tr("editor.art.link_target_changed", "The project, image or import settings changed before linking. Open Edit Source Art again.")))
|
|
return
|
|
}
|
|
for name := range guards {
|
|
if source := docs.Script(name); source != nil && (source.Dirty() || source.Conflict) {
|
|
fail(fmt.Errorf("%s", i18n.Tr("editor.art.resolve_source_settings", "Save or resolve unsaved source settings before linking: {path}", i18n.Str("path", name))))
|
|
return
|
|
}
|
|
}
|
|
target := strings.TrimSpace(values[0])
|
|
if !strings.EqualFold(path.Ext(target), art.SourceExtension) {
|
|
fail(fmt.Errorf("%s", i18n.Tr("editor.art.link_extension", "The editable source must use .aeart.")))
|
|
return
|
|
}
|
|
newMeta, err := pl.NewSourceMeta(target)
|
|
if err != nil {
|
|
fail(err)
|
|
return
|
|
}
|
|
for _, key := range []string{"filter", "colorspace", "srgb"} {
|
|
if value, ok := meta.Settings[key]; ok {
|
|
newMeta.Settings[key] = value
|
|
}
|
|
}
|
|
linkedMeta, err := pipeline.ParseMeta(metaBefore, sourcePath+pipeline.MetaSuffix)
|
|
if err == nil {
|
|
err = artsource.SetTextureLink(linkedMeta, newMeta.ID, before, frames)
|
|
}
|
|
if err != nil {
|
|
fail(err)
|
|
return
|
|
}
|
|
edits := []pipeline.SourceEdit{
|
|
{Path: target, After: encoded},
|
|
{Path: target + pipeline.MetaSuffix, After: pipeline.WriteMeta(newMeta)},
|
|
{Path: sourcePath + pipeline.MetaSuffix, Before: metaBefore, After: pipeline.WriteMeta(linkedMeta)},
|
|
}
|
|
paths := make([]string, 0, len(guards))
|
|
for name := range guards {
|
|
if name != sourcePath+pipeline.MetaSuffix {
|
|
paths = append(paths, name)
|
|
}
|
|
}
|
|
slices.Sort(paths)
|
|
for _, name := range paths {
|
|
// Normalization of inputs must not silently change source PNGs,
|
|
// linked surface files, or their metadata during the transaction.
|
|
edits = append(edits, pipeline.SourceEdit{Path: name, Before: guards[name], After: guards[name], ReadOnlyGuard: true})
|
|
}
|
|
batch, err := pl.ApplySources(edits, false)
|
|
if err != nil {
|
|
fail(err)
|
|
return
|
|
}
|
|
ed.ApplyBatch(batch)
|
|
// ApplySources records the actual canonicalized metadata generation.
|
|
if err := docs.AcceptSavedScript(metadata, metaBefore, edits[2].After); err != nil {
|
|
fail(err)
|
|
return
|
|
}
|
|
link, err := artsource.TextureLinkOf(linkedMeta)
|
|
if err == nil {
|
|
err = openLinkedArtFrame(ed, link, selected)
|
|
}
|
|
if err != nil {
|
|
fail(err)
|
|
}
|
|
}
|
|
// A path prompt renders its message on one line. Use the scrollable
|
|
// confirmation first so the shared-resource and metadata warning is visible.
|
|
ed.Ask(context.Question{
|
|
Title: i18n.Tr("editor.art.create_linked_source", "Create linked art source"),
|
|
Message: i18n.Tr("editor.art.linked_source_notice", "Create an editable source for {path} and store a link in its image import metadata. Sprite and animation IDs stay the same; edits affect every consumer of this shared asset. The original PNG is preserved.", i18n.Str("path", sourcePath)),
|
|
Buttons: []string{i18n.Tr("editor.shell.continue", "Continue"), i18n.Tr("editor.art.cancel", "Cancel")},
|
|
Then: func(choice int) {
|
|
if choice != 0 {
|
|
return
|
|
}
|
|
if !current() {
|
|
ed.Notify(context.LevelError, i18n.Tr("editor.art.link_target_changed", "The project, image or import settings changed before linking. Open Edit Source Art again."))
|
|
return
|
|
}
|
|
ed.Ask(context.Question{
|
|
Title: i18n.Tr("editor.art.create_linked_source", "Create linked art source"),
|
|
Fields: []string{i18n.Tr("editor.art.source_path", "Source path")},
|
|
Values: []string{assetops.UniqueFile(ed, path.Dir(sourcePath), stem, art.SourceExtension)},
|
|
Buttons: []string{i18n.Tr("editor.art.create_linked", "Create linked source"), i18n.Tr("editor.art.cancel", "Cancel")},
|
|
Submit: submit,
|
|
})
|
|
},
|
|
})
|
|
return nil
|
|
}
|