262 lines
6.3 KiB
Go
262 lines
6.3 KiB
Go
package prefabs
|
|
|
|
import (
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"aego/asset"
|
|
"aego/core/errs"
|
|
"aego/core/ids"
|
|
"aego/core/log"
|
|
"aego/editor/core/assetops"
|
|
"aego/editor/core/clipboard"
|
|
"aego/editor/core/context"
|
|
"aego/editor/core/document"
|
|
"aego/editor/core/history/commands"
|
|
"aego/project/refactor"
|
|
"aego/scene"
|
|
"aego/scene/mutate"
|
|
"aego/scene/prefab"
|
|
"aego/scene/sceneio"
|
|
)
|
|
|
|
const DefaultDir = "prefabs"
|
|
|
|
type Service struct {
|
|
ctx *context.Editor
|
|
infos map[infoKey]*Info
|
|
writing bool
|
|
fixed *sceneio.Library
|
|
}
|
|
|
|
func New(ctx *context.Editor) *Service {
|
|
return &Service{ctx: ctx, infos: map[infoKey]*Info{}}
|
|
}
|
|
|
|
func (s *Service) SetLibrary(lib *sceneio.Library) {
|
|
s.fixed = lib
|
|
clear(s.infos)
|
|
}
|
|
|
|
func (s *Service) lib() *sceneio.Library {
|
|
if s.fixed != nil {
|
|
return s.fixed
|
|
}
|
|
var res asset.Resolver
|
|
if s.ctx.Loader != nil {
|
|
res = s.ctx.Loader
|
|
}
|
|
return sceneio.NewLibrary(s.ctx.Types.Schema(), res)
|
|
}
|
|
|
|
func (s *Service) editable() error {
|
|
if s.ctx.Docs.InPlay() {
|
|
return errs.New(errs.EditorCommand, "prefabs cannot be changed during Play")
|
|
}
|
|
if s.ctx.Pipeline == nil {
|
|
return errs.New(errs.EditorCommand, "the asset pipeline is not available")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) pathOf(id ids.AssetID) (string, bool) {
|
|
if s.ctx.Pipeline == nil {
|
|
return "", false
|
|
}
|
|
return s.ctx.Pipeline.PathOf(id)
|
|
}
|
|
|
|
func (s *Service) stem(id ids.AssetID) string {
|
|
rel, ok := s.pathOf(id)
|
|
if !ok {
|
|
return id.Short()
|
|
}
|
|
return strings.TrimSuffix(path.Base(rel), path.Ext(rel))
|
|
}
|
|
|
|
func fileSafe(name string) string {
|
|
name = strings.Map(func(r rune) rune {
|
|
if r < 0x20 || strings.ContainsRune(`/\:*?"<>|`, r) {
|
|
return '-'
|
|
}
|
|
return r
|
|
}, strings.TrimSpace(name))
|
|
name = strings.TrimLeft(name, ".")
|
|
if name == "" {
|
|
return "Prefab"
|
|
}
|
|
return name
|
|
}
|
|
|
|
func (s *Service) Open(id ids.AssetID) error {
|
|
rel, ok := s.pathOf(id)
|
|
if !ok {
|
|
return errs.New(errs.EditorDocument, "prefab "+id.Short()+" is not in the project")
|
|
}
|
|
var from document.ID
|
|
if d := s.ctx.Docs.Edit(); d != nil {
|
|
from = d.ID
|
|
}
|
|
_, err := s.ctx.Docs.OpenFrom(rel, from)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) SelectAsset(id ids.AssetID) {
|
|
if rel, ok := s.pathOf(id); ok {
|
|
s.ctx.Open(context.AssetAt(rel))
|
|
}
|
|
}
|
|
|
|
func (s *Service) FindInstances(id ids.AssetID) {
|
|
lib := s.lib()
|
|
changed := map[ids.AssetID]bool{id: true}
|
|
active := s.ctx.Doc()
|
|
var here []ids.NodeID
|
|
elsewhere := 0
|
|
for _, d := range s.ctx.Docs.Docs() {
|
|
sc := d.Scene()
|
|
for _, h := range sc.PreOrder() {
|
|
pi, ok := sceneio.InstanceOf(sc, h)
|
|
if !ok || !pi.Owner || !sc.IsAlive(h) || !s.dependsOn(lib, pi.Prefab, changed) {
|
|
continue
|
|
}
|
|
if active != nil && d.ID == active.ID {
|
|
here = append(here, sc.ID(h))
|
|
} else {
|
|
elsewhere++
|
|
}
|
|
}
|
|
}
|
|
if len(here) > 0 {
|
|
s.ctx.Select(here...)
|
|
}
|
|
msg := strconv.Itoa(len(here)) + " instance(s) of " + s.stem(id) + " in this scene"
|
|
if elsewhere > 0 {
|
|
msg += ", " + strconv.Itoa(elsewhere) + " in other open scenes"
|
|
}
|
|
var files []string
|
|
for _, dep := range s.ctx.Pipeline.Dependents(id) {
|
|
if rel, ok := s.ctx.Pipeline.PathOf(dep); ok {
|
|
files = append(files, rel)
|
|
}
|
|
}
|
|
if len(files) > 0 {
|
|
msg += "; used by " + strings.Join(files, ", ")
|
|
}
|
|
s.ctx.Notify(context.LevelInfo, msg)
|
|
}
|
|
|
|
func (s *Service) Space(id ids.AssetID) (scene.Space, bool) {
|
|
lib := s.lib()
|
|
p, ok := lib.Prefab(id)
|
|
if !ok {
|
|
return scene.Space2D, false
|
|
}
|
|
t, _, err := prefab.Expand(lib, p, nil)
|
|
if err != nil {
|
|
return scene.Space2D, false
|
|
}
|
|
if t.Space3D {
|
|
return scene.Space3D, true
|
|
}
|
|
return scene.Space2D, true
|
|
}
|
|
|
|
func (s *Service) Build(sc *scene.Scene, parent scene.NodeHandle, id ids.AssetID) (scene.NodeHandle, error) {
|
|
if parent.IsZero() {
|
|
parent = sc.Root()
|
|
}
|
|
data := sceneio.EncodeInstance(sc.Registry(), ids.NewNodeID(), &prefab.Instance{Prefab: id})
|
|
created, _, err := sceneio.LoadInto(sc, parent, data, "prefab", sceneio.LoadOptions{KeepIDs: true, Prefabs: s.lib()})
|
|
if err != nil {
|
|
return scene.NodeHandle{}, err
|
|
}
|
|
for _, h := range created {
|
|
if sc.Parent(h) == parent {
|
|
return h, nil
|
|
}
|
|
}
|
|
return scene.NodeHandle{}, errs.New(errs.EditorCommand, "prefab has no root node", log.F("prefab", id.String()))
|
|
}
|
|
|
|
func (s *Service) CreateFrom(d *document.SceneDocument, nodes []ids.NodeID, dir string) error {
|
|
if err := s.editable(); err != nil {
|
|
return err
|
|
}
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
if dir == "" {
|
|
dir = DefaultDir
|
|
}
|
|
sc := d.Scene()
|
|
tops := clipboard.TopLevel(sc, nodes)
|
|
if len(tops) == 0 {
|
|
return nil
|
|
}
|
|
if err := assetops.EnsureDir(s.ctx, dir); err != nil {
|
|
return err
|
|
}
|
|
tx := d.History.Begin("Create Prefab")
|
|
for _, h := range tops {
|
|
if err := refactor.CheckCreate(sc, h); err != nil {
|
|
tx.Abort()
|
|
return err
|
|
}
|
|
rel := assetops.UniqueFile(s.ctx, dir, fileSafe(sc.Name(h)), document.PrefabExt)
|
|
before := sceneio.SaveSubtree(sc, h, true)
|
|
id, err := assetops.CreateFile(s.ctx, rel, before)
|
|
if err != nil {
|
|
tx.Abort()
|
|
return err
|
|
}
|
|
after, err := refactor.LinkBlock(sc, h, s.lib(), id)
|
|
if err != nil {
|
|
tx.Abort()
|
|
return err
|
|
}
|
|
if err := tx.Exec(commands.NewMutation("Create Prefab", s.env(sc), mutate.Replace(commands.Ref(sc.ID(h)), string(after)))); err != nil {
|
|
tx.Abort()
|
|
return err
|
|
}
|
|
s.ctx.Notify(context.LevelInfo, "Created "+rel)
|
|
}
|
|
tx.Commit()
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) CreateVariant(id ids.AssetID) error {
|
|
if err := s.editable(); err != nil {
|
|
return err
|
|
}
|
|
rel, ok := s.pathOf(id)
|
|
if !ok {
|
|
return errs.New(errs.EditorDocument, "prefab "+id.Short()+" is not in the project")
|
|
}
|
|
stem := strings.TrimSuffix(path.Base(rel), path.Ext(rel))
|
|
dst := assetops.UniqueFile(s.ctx, path.Dir(rel), stem+" Variant", document.PrefabExt)
|
|
root := &prefab.Node{Local: ids.NewNodeID(), Instance: &prefab.Instance{Prefab: id}}
|
|
if _, err := assetops.CreateFile(s.ctx, dst, sceneio.EncodePrefab(s.ctx.Types.Schema(), root)); err != nil {
|
|
return err
|
|
}
|
|
_, err := s.ctx.Docs.Open(dst)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) SaveDocument(d *document.SceneDocument) error {
|
|
id, ok := s.ctx.Pipeline.Lookup(d.Path)
|
|
if !ok || !d.OnDisk || s.ctx.Docs.InPlay() {
|
|
return s.ctx.Docs.Save(d.ID)
|
|
}
|
|
before, err := readFile(s.ctx.Pipeline.Abs(d.Path))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f := fileChange{id: id, rel: d.Path, before: before, after: sceneio.SavePrefab(d.Scene())}
|
|
if err := s.commit("Save "+path.Base(d.Path), []fileChange{f}, nil); err != nil {
|
|
return err
|
|
}
|
|
return s.ctx.Docs.Save(d.ID)
|
|
}
|