4 Commits
Author SHA1 Message Date
sanspie cffa66514b Stage 9.6c2: prefab apply and data editing without the editor (D298)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 21m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 22m8s
2026-10-20 13:05:10 +03:00
sanspie 2744cff481 Stage 9.6c1: project refs and refactorings without the editor (D298)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m31s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 21m57s
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
2026-10-20 12:12:18 +03:00
sanspie 91052a54db make lint: staticcheck the editor build
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 16m26s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m48s
2026-10-20 12:06:56 +03:00
sanspie 7f4d87ef2c Stage 9: gameplay toolkit, export and web, agent tooling 2026-10-20 12:06:56 +03:00
83 changed files with 6466 additions and 1630 deletions
+8 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -98,6 +98,7 @@ vet: bootstrap
lint: bootstrap
go run honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION) $(LINT_PKGS)
go run honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION) -tags aego_editor ./editor/... ./app/... ./tests/...
# All graph variants, the semantic pass and the negative rule tests.
depcheck:
+2 -6
View File
@@ -116,12 +116,8 @@ func TestPlayLeavesEditSceneByteIdentical(t *testing.T) {
t.Fatal("Spin is not registered")
}
speed, _ := td.Field("speed")
add := &commands.AddComponent{
Node: spinner,
Type: td.ID,
Name: td.FullName,
Values: []commands.FieldValue{{Field: speed.ID, Value: schema.Float32(3)}},
}
add := commands.NewMutation("Add "+td.FullName, commands.EnvFor(es),
commands.AddComp(es, spinner, td, []commands.FieldValue{{Field: speed.ID, Value: schema.Float32(3)}}))
if err := d.History.Exec(add); err != nil {
t.Fatal(err)
}
+1 -1
View File
@@ -42,7 +42,7 @@ func TestColliderFitsSprite(t *testing.T) {
}
for _, name := range []string{"Crate", "Bare"} {
id := es.ID(es.Find(es.Root(), name))
if err := d.History.Exec(&commands.AddComponent{Node: id, Type: td.ID, Name: td.FullName, OnAdd: ctx.OnAdd(td)}); err != nil {
if err := d.History.Exec(commands.NewMutation("Add "+td.FullName, commands.WithOnAdd(commands.EnvFor(es), ctx.OnAdd), commands.AddComp(es, id, td, nil))); err != nil {
t.Fatal(err)
}
}
+2 -6
View File
@@ -51,12 +51,8 @@ func openSpinner(t *testing.T) *spinFixture {
}
fd, _ := td.Field("speed")
f.typ, f.speed = td.ID, fd.ID
add := &commands.AddComponent{
Node: f.spinner,
Type: td.ID,
Name: td.FullName,
Values: []commands.FieldValue{{Field: fd.ID, Value: schema.Float32(3)}},
}
add := commands.NewMutation("Add "+td.FullName, commands.EnvFor(es),
commands.AddComp(es, f.spinner, td, []commands.FieldValue{{Field: fd.ID, Value: schema.Float32(3)}}))
if err := d.History.Exec(add); err != nil {
t.Fatal(err)
}
+6 -11
View File
@@ -3,7 +3,6 @@ package assetops
import (
"os"
"path"
"regexp"
"sort"
"strconv"
"strings"
@@ -12,6 +11,8 @@ import (
"aego/core/ids"
"aego/editor/core/context"
"aego/editor/core/document"
"aego/project/refactor"
"aego/project/rename"
)
const (
@@ -19,8 +20,6 @@ const (
opsLimit = 64
)
var packageDecl = regexp.MustCompile(`(?m)^[ \t]*package[ \t]+([A-Za-z_][A-Za-z0-9_]*)`)
type FileOp struct {
Dir bool
ID ids.AssetID
@@ -180,8 +179,8 @@ func setPackage(ctx *context.Editor, rel, pkg string) error {
return err
}
c := d.Code
if m := packageDecl.FindSubmatchIndex(c.Bytes()); m != nil {
c.ReplaceRanges([][2]int{{m[2], m[3]}}, [][]byte{[]byte(pkg)})
if a, b, ok := refactor.PackageSpan(c.Bytes()); ok {
c.ReplaceRanges([][2]int{{a, b}}, [][]byte{[]byte(pkg)})
} else {
c.ReplaceRanges([][2]int{{0, 0}}, [][]byte{[]byte("package " + pkg + "\n\n")})
}
@@ -207,7 +206,7 @@ func Stem(p string) string {
}
func ValidName(s string) bool {
return s != "" && s != "." && s != ".." && !strings.HasPrefix(s, ".") && !strings.ContainsAny(s, "/\\")
return rename.ValidName(s)
}
func ValidIdent(s string) bool {
@@ -270,11 +269,7 @@ func ScriptsRel(dir string) (string, bool) {
}
func PackageOf(ctx *context.Editor, dir string) string {
if rel, ok := ScriptsRel(dir); ok && rel != "" {
return path.Base(rel)
}
m := moduleOf(ctx)
return m[strings.LastIndexByte(m, '/')+1:]
return refactor.PackageOf(ctx.Project, dir)
}
func FileStem(name string) string {
+2 -4
View File
@@ -9,6 +9,7 @@ import (
"aego/core/ids"
"aego/editor/core/context"
"aego/editor/core/history/commands"
"aego/project/refactor"
"aego/scene/scriptcomp"
"aego/schema"
)
@@ -20,10 +21,7 @@ func shortName(full string) string {
}
func moduleOf(ctx *context.Editor) string {
if ctx.Project != nil && ctx.Project.Module != "" {
return ctx.Project.Module
}
return "game"
return refactor.Module(ctx.Project)
}
func ScriptPrefix(ctx *context.Editor, rel string) string {
+22 -18
View File
@@ -10,6 +10,7 @@ import (
"aego/editor/core/history/commands"
"aego/format/aetext"
"aego/scene"
"aego/scene/mutate"
"aego/scene/sceneio"
)
@@ -237,12 +238,24 @@ func Paste(ctx *context.Editor) error {
if err != nil {
return err
}
frags, err := commands.Fragments(data)
if err != nil {
return err
}
parent, index := pasteTarget(ctx)
cmd := &commands.Create{Parent: parent, Index: index, Fragment: data, Name: "Paste"}
ops := make([]mutate.Op, len(frags))
for i, f := range frags {
at := -1
if index >= 0 {
at = index + i
}
ops[i] = commands.Insert(parent, at, f)
}
cmd := commands.NewMutation("Create Paste", commands.EnvFor(d.Scene()), ops...)
if err := d.History.Exec(cmd); err != nil {
return err
}
ctx.Select(append([]ids.NodeID(nil), cmd.Roots()...)...)
ctx.Select(cmd.Created()...)
return nil
}
@@ -267,17 +280,13 @@ func Duplicate(ctx *context.Editor) error {
tx.Abort()
return err
}
c := &commands.Create{
Parent: s.ID(s.Parent(h)),
Index: commands.ChildIndex(s, h) + 1,
Fragment: data,
Name: s.Name(h),
}
c := commands.NewMutation("Create "+s.Name(h), commands.EnvFor(s),
commands.Insert(s.ID(s.Parent(h)), commands.ChildIndex(s, h)+1, string(data)))
if err := tx.Exec(c); err != nil {
tx.Abort()
return err
}
created = append(created, c.Roots()...)
created = append(created, c.Created()...)
}
tx.Commit()
ctx.Select(created...)
@@ -294,18 +303,13 @@ func Delete(ctx *context.Editor) error {
if len(tops) == 0 {
return nil
}
targets := make([]ids.NodeID, len(tops))
ops := make([]mutate.Op, len(tops))
for i, h := range tops {
targets[i] = s.ID(h)
ops[i] = mutate.RemoveNode(commands.Ref(s.ID(h)))
}
tx := d.History.Begin("Delete")
for _, id := range targets {
if err := tx.Exec(&commands.Delete{Node: id}); err != nil {
tx.Abort()
return err
}
if err := d.History.Exec(commands.NewMutation("Delete", commands.EnvFor(s), ops...)); err != nil {
return err
}
tx.Commit()
ctx.Select()
return nil
}
+5 -17
View File
@@ -113,8 +113,7 @@ func (e Env) encodeComponent(td *schema.TypeDescriptor, values []commands.FieldV
sc := e.scratch()
h := sc.Create(sc.Root(), "component")
sc.Flush()
add := &commands.AddComponent{Node: sc.ID(h), Type: td.ID, Name: td.FullName, Values: values}
if err := add.Do(target{sc}); err != nil {
if err := putComponent(sc, h, td, values); err != nil {
return nil, err
}
return sceneio.SaveSubtree(sc, h, true), nil
@@ -122,18 +121,14 @@ func (e Env) encodeComponent(td *schema.TypeDescriptor, values []commands.FieldV
func (e Env) decodeComponent(data []byte) (*schema.TypeDescriptor, []commands.FieldValue, error) {
sc := e.scratch()
c := &commands.Create{Index: -1, Fragment: data}
if err := c.Do(target{sc}); err != nil {
root, err := loadScratch(sc, data)
if err != nil {
return nil, nil, err
}
roots := c.Roots()
if len(roots) == 0 {
return nil, nil, errs.New(errs.EditorCommand, "clipboard component is empty")
}
reg := sc.Registry()
unknown, hasUnknown := reg.ByName("engine.Unknown")
var td *schema.TypeDescriptor
sc.ComponentsOf(sc.Handle(roots[0]), func(t schema.TypeID) {
sc.ComponentsOf(sc.Handle(root), func(t schema.TypeID) {
if td != nil || (hasUnknown && t == unknown.ID) {
return
}
@@ -142,7 +137,7 @@ func (e Env) decodeComponent(data []byte) (*schema.TypeDescriptor, []commands.Fi
if td == nil {
return nil, nil, errs.New(errs.EditorCommand, "clipboard component type is not loaded in this editor")
}
values, err := componentValues(sc, roots[0], td)
values, err := componentValues(sc, root, td)
if err != nil {
return nil, nil, err
}
@@ -189,13 +184,6 @@ func (e Env) ReadComponent() (*schema.TypeDescriptor, []commands.FieldValue, boo
return td, values, true
}
func (e Env) onAdd(td *schema.TypeDescriptor) func(*scene.Scene, scene.NodeHandle) {
if e.OnAdd == nil {
return nil
}
return e.OnAdd(td)
}
func (e Env) PasteComponent(d *document.SceneDocument, nodes []ids.NodeID) error {
td, values, ok := e.ReadComponent()
if !ok {
+28
View File
@@ -0,0 +1,28 @@
package clipboard
import (
"aego/core/errs"
"aego/core/ids"
"aego/editor/core/history/commands"
"aego/scene"
"aego/scene/sceneio"
"aego/schema"
)
func loadScratch(sc *scene.Scene, data []byte) (ids.NodeID, error) {
root := sc.Root()
created, _, err := sceneio.LoadInto(sc, root, data, "fragment", sceneio.LoadOptions{KeepIDs: true})
if err != nil {
return ids.NodeID{}, err
}
for _, h := range created {
if sc.Parent(h) == root {
return sc.ID(h), nil
}
}
return ids.NodeID{}, errs.New(errs.EditorCommand, "the fragment holds no node")
}
func putComponent(sc *scene.Scene, h scene.NodeHandle, td *schema.TypeDescriptor, list []commands.FieldValue) error {
return commands.NewMutation("", commands.EnvFor(sc), commands.AddRaw(sc, sc.ID(h), td, list)).Do(target{sc})
}
+8 -12
View File
@@ -22,6 +22,7 @@ type compData struct {
func (e Env) pasteInto(tx *history.Transaction, d *document.SceneDocument, nodes []ids.NodeID, td *schema.TypeDescriptor, values []commands.FieldValue) error {
s := d.Scene()
env := commands.WithOnAdd(commands.EnvFor(s), e.OnAdd)
for _, n := range nodes {
h := s.Handle(n)
if h.IsZero() || !s.IsAlive(h) {
@@ -31,7 +32,7 @@ func (e Env) pasteInto(tx *history.Transaction, d *document.SceneDocument, nodes
if !s.Accepts(h, td.ID) {
continue
}
if err := tx.Exec(&commands.AddComponent{Node: n, Type: td.ID, Name: td.FullName, Values: values, OnAdd: e.onAdd(td)}); err != nil {
if err := tx.Exec(commands.NewMutation("Add "+td.FullName, env, commands.AddComp(s, n, td, values))); err != nil {
return err
}
continue
@@ -94,8 +95,7 @@ func (e Env) CopyComponents(s *scene.Scene, node ids.NodeID, types []schema.Type
if err != nil {
return err
}
add := &commands.AddComponent{Node: sc.ID(h), Type: td.ID, Name: td.FullName, Values: values}
if err := add.Do(target{sc}); err != nil {
if err := putComponent(sc, h, td, values); err != nil {
return err
}
}
@@ -118,11 +118,10 @@ func (e Env) readComponents() ([]compData, bool) {
data = []byte(strings.TrimPrefix(t, componentsText))
}
sc := e.scratch()
cr := &commands.Create{Index: -1, Fragment: data}
if err := cr.Do(target{sc}); err != nil || len(cr.Roots()) == 0 {
root, err := loadScratch(sc, data)
if err != nil {
return nil, false
}
root := cr.Roots()[0]
reg := sc.Registry()
unknown, hasUnknown := reg.ByName("engine.Unknown")
var out []compData
@@ -186,14 +185,11 @@ func (e Env) EncodeScript(s *scene.Scene, node ids.NodeID, name string) ([]byte,
func (e Env) decodeScript(data []byte) (string, schema.PropertyBag, error) {
sc := e.scratch()
cr := &commands.Create{Index: -1, Fragment: data}
if err := cr.Do(target{sc}); err != nil {
root, err := loadScratch(sc, data)
if err != nil {
return "", schema.PropertyBag{}, err
}
if len(cr.Roots()) == 0 {
return "", schema.PropertyBag{}, errs.New(errs.EditorCommand, "the preset is empty")
}
scs, ok := scriptcomp.ScriptsOf(sc, sc.Handle(cr.Roots()[0]))
scs, ok := scriptcomp.ScriptsOf(sc, sc.Handle(root))
if !ok || len(scs.Attachments) == 0 {
return "", schema.PropertyBag{}, errs.New(errs.EditorCommand, "the preset holds no script")
}
File diff suppressed because it is too large Load Diff
+15 -10
View File
@@ -15,6 +15,7 @@ import (
"aego/data"
"aego/editor/core/context"
"aego/editor/core/document"
"aego/scene/mutate"
"aego/schema"
)
@@ -108,6 +109,18 @@ func (s *Service) file(id ids.AssetID) (string, []byte, error) {
return rel, raw, nil
}
func (s *Service) env() mutate.Env {
env := mutate.Env{Types: s.ctx.Types}
if pl := s.ctx.Pipeline; pl != nil {
env.Lookup = pl.Lookup
env.Assets = func(id ids.AssetID) bool {
_, ok := pl.PathOf(id)
return ok || asset.IsBuiltin(id)
}
}
return env
}
func (s *Service) Doc(id ids.AssetID) (*Doc, error) {
d := s.docs[id]
if d != nil && d.rev == s.ctx.AssetRev {
@@ -125,11 +138,7 @@ func (s *Service) Doc(id ids.AssetID) (*Doc, error) {
if err != nil {
return nil, err
}
parsed, err := data.ParseDoc(raw, rel)
if err != nil {
return nil, err
}
nd, err := NewDoc(s.reg(), lib, id, rel, parsed)
nd, err := NewDoc(s.env(), lib, id, rel, raw)
if err != nil {
return nil, err
}
@@ -151,11 +160,7 @@ func (s *Service) Table(id ids.AssetID) (*Table, error) {
t.Path, t.rev = rel, s.ctx.AssetRev
return t, nil
}
parsed, err := data.ParseTable(raw, rel)
if err != nil {
return nil, err
}
nt, err := NewTable(s.reg(), id, rel, parsed)
nt, err := NewTable(s.env(), id, rel, raw)
if err != nil {
return nil, err
}
File diff suppressed because it is too large Load Diff
+2 -23
View File
@@ -20,6 +20,7 @@ import (
"aego/editor/core/selection"
"aego/engine"
"aego/engine/module"
"aego/project"
"aego/scene"
"aego/scene/sceneio"
)
@@ -441,29 +442,7 @@ func (m *Manager) UniquePath(rel string) string {
}
func WriteAtomic(abs string, data []byte) error {
dir := filepath.Dir(abs)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(dir, ".aego-*.tmp")
if err != nil {
return err
}
name := tmp.Name()
if _, err := tmp.Write(data); err != nil {
tmp.Close()
os.Remove(name)
return err
}
if err := tmp.Close(); err != nil {
os.Remove(name)
return err
}
if err := os.Rename(name, abs); err != nil {
os.Remove(name)
return err
}
return nil
return project.WriteAtomic(abs, data)
}
func encode(d *SceneDocument) []byte {
+13 -2
View File
@@ -1,6 +1,7 @@
package gizmo
import (
"aego/core/errs"
"aego/editor/core/history/commands"
"aego/editor/core/props"
"aego/schema"
@@ -14,15 +15,25 @@ func SetProperty(e *EditCtx, t schema.TypeID, fd *schema.FieldDescriptor, v sche
if h.IsZero() {
return
}
td, ok := e.Scene.Registry().ByID(t)
if !ok {
e.Err = errs.New(errs.SchemaUnknownType, "component type is not registered")
return
}
nv, err := props.Normalize(fd, v)
if err != nil {
e.Err = err
return
}
before, err := e.Scene.GetProperty(h, t, fd.ID)
if err != nil {
e.Err = err
return
}
if props.Equal(before, v) {
if props.Equal(before, nv) {
return
}
if err := e.Tx.Exec(&commands.SetProperty{Node: e.Node, Type: t, Field: fd.ID, Before: before, After: v, Name: props.Label(fd)}); err != nil {
if err := e.Tx.Exec(commands.SetField(e.Scene, e.Node, td, fd, nv, props.Label(fd))); err != nil {
e.Err = err
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-51
View File
@@ -3,61 +3,10 @@ package commands
import (
"aego/core/errs"
"aego/core/ids"
"aego/core/log"
"aego/editor/core/history"
"aego/scene"
"aego/scene/sceneio"
)
type Replace struct {
Node ids.NodeID
Before []byte
After []byte
Name string
Post func(s *scene.Scene)
parent ids.NodeID
index int
ready bool
}
func (c *Replace) Label() string {
if c.Name != "" {
return c.Name
}
return "Replace"
}
func (c *Replace) Do(t history.Target) error {
s := t.Scene()
if err := c.swap(s, c.After); err != nil {
return err
}
if c.Post != nil {
c.Post(s)
}
return nil
}
func (c *Replace) Undo(t history.Target) error {
return c.swap(t.Scene(), c.Before)
}
func (c *Replace) Merge(history.Command) bool {
return false
}
func (c *Replace) swap(s *scene.Scene, data []byte) error {
if !c.ready {
h := s.Handle(c.Node)
if h.IsZero() || !s.IsAlive(h) {
return errs.New(errs.SceneDeadNode, "node is not in the scene", log.F("node", c.Node.String()))
}
c.parent, c.index, c.ready = s.ID(s.Parent(h)), ChildIndex(s, h), true
}
_, err := ReplaceNode(s, c.Node, c.parent, c.index, data, nil)
return err
}
func ReplaceNode(s *scene.Scene, node, parent ids.NodeID, index int, data []byte, lib *sceneio.Library) (scene.NodeHandle, error) {
if h := s.Handle(node); !h.IsZero() && s.IsAlive(h) {
s.Destroy(h)
+4 -4
View File
@@ -18,7 +18,7 @@ func TestReparent3DPreservesWorld(t *testing.T) {
s.Flush()
before := s.World4(n)
h := history.New(target{s})
if err := h.Exec(&Reparent{Node: s.ID(n), NewParent: s.ID(p), NewIndex: -1, KeepWorld: true}); err != nil {
if err := h.Exec(reparent(s, n, p)); err != nil {
t.Fatal(err)
}
if s.Parent(n) != p {
@@ -39,7 +39,7 @@ func TestReparent3DUnderTwoDFails(t *testing.T) {
n := s.Create3D(s.Root(), "N")
s.Flush()
h := history.New(target{s})
err := h.Exec(&Reparent{Node: s.ID(n), NewParent: s.ID(flat), NewIndex: -1, KeepWorld: true})
err := h.Exec(reparent(s, n, flat))
if !errs.Has(err, errs.SceneSpaceMismatch) {
t.Fatalf("err = %v", err)
}
@@ -57,7 +57,7 @@ func TestReparent2DIntoThreeDKeepsWorld(t *testing.T) {
s.Flush()
before := s.World4(n)
h := history.New(target{s})
if err := h.Exec(&Reparent{Node: s.ID(n), NewParent: s.ID(p), NewIndex: -1, KeepWorld: true}); err != nil {
if err := h.Exec(reparent(s, n, p)); err != nil {
t.Fatal(err)
}
if !s.Under3D(n) || !s.World4(n).NearlyEqual(before, 1e-4) {
@@ -97,7 +97,7 @@ func TestDeleteUndoKeepsSpace(t *testing.T) {
s.Flush()
id := s.ID(n)
h := history.New(target{s})
if err := h.Exec(&Delete{Node: id}); err != nil {
if err := h.Exec(del(s, id)); err != nil {
t.Fatal(err)
}
h.Undo()
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -480,6 +480,7 @@ func (c *Controller) CopyToEdit(nodes []ids.NodeID, t schema.TypeID) error {
if !ok {
return errs.New(errs.SchemaUnknownType, "component type is not registered")
}
env := commands.EnvFor(es)
tx := real.History.Begin("Copy " + td.FullName + " from Play")
for _, n := range nodes {
if _, has := es.GetByType(es.Handle(n), t); !has {
@@ -492,7 +493,7 @@ func (c *Controller) CopyToEdit(nodes []ids.NodeID, t schema.TypeID) error {
}
values = append(values, commands.FieldValue{Field: td.Fields[i].ID, Value: v})
}
if err := tx.Exec(&commands.AddComponent{Node: n, Type: t, Name: td.FullName, Values: values}); err != nil {
if err := tx.Exec(commands.NewMutation("Add "+td.FullName, env, commands.AddComp(es, n, td, values))); err != nil {
tx.Abort()
return err
}
+12 -2
View File
@@ -3,6 +3,7 @@ package playdiff
import (
"reflect"
"strconv"
"strings"
"aego/core/curve"
"aego/core/ids"
@@ -12,6 +13,7 @@ import (
"aego/editor/core/history/commands"
"aego/editor/core/props"
"aego/scene"
"aego/scene/mutate"
"aego/scene/scriptcomp"
"aego/schema"
)
@@ -94,8 +96,16 @@ func Touched(h *history.History) map[Key]bool {
}
h.Walk(func(c history.Command) {
switch c := c.(type) {
case *commands.SetProperty:
out[Key{Node: c.Node, Type: c.Type, Field: c.Field}] = true
case *commands.Mutation:
for _, ch := range c.Changes() {
switch {
case ch.Op != mutate.KindSet && ch.Op != mutate.KindReset:
case ch.Type != 0:
out[Key{Node: ch.NodeID, Type: ch.Type, Field: ch.Field}] = true
case strings.HasPrefix(ch.Path, "transform."):
out[Key{Node: ch.NodeID}] = true
}
}
case *commands.SetTransform:
out[Key{Node: c.Node}] = true
case *commands.SetTransform3D:
+12 -1
View File
@@ -45,7 +45,7 @@ func pair(t *testing.T) (*document.SceneDocument, *document.SceneDocument, ids.N
}
s.Flush()
d := m.New(rel, s)
if err := d.History.Exec(&commands.AddComponent{Node: id, Type: td.ID, Name: td.FullName}); err != nil {
if err := d.History.Exec(commands.NewMutation("Add "+td.FullName, commands.EnvFor(s), commands.AddComp(s, id, td, nil))); err != nil {
t.Fatal(err)
}
docs[i] = d
@@ -108,3 +108,14 @@ func TestApplyIsOneStep(t *testing.T) {
t.Fatal("undo must restore the edit scene")
}
}
func TestTouchedSeesInspectorEdits(t *testing.T) {
_, play, id, td := pair(t)
fd, _ := td.Field("tint")
if err := play.Props.Set(nil, []ids.NodeID{id}, td.ID, fd.ID, schema.Color(mathx.Color{G: 1, A: 1})); err != nil {
t.Fatal(err)
}
if !playdiff.Touched(play.History)[playdiff.Key{Node: id, Type: td.ID, Field: fd.ID}] {
t.Fatal("a field set through the Inspector is not touched")
}
}
File diff suppressed because it is too large Load Diff
+12 -19
View File
@@ -3,7 +3,6 @@ package prefabs
import (
"os"
"slices"
"strings"
"aego/core/errs"
"aego/core/ids"
@@ -11,6 +10,7 @@ import (
"aego/editor/core/document"
"aego/editor/core/history"
"aego/editor/core/history/commands"
"aego/project/refactor"
"aego/scene"
"aego/scene/prefab"
"aego/scene/sceneio"
@@ -80,25 +80,18 @@ func readFile(abs string) ([]byte, error) {
return data, nil
}
func (s *Service) files(edits map[ids.AssetID]*prefab.Node) ([]fileChange, error) {
reg := s.ctx.Types.Schema()
var out []fileChange
for id, m := range edits {
rel, ok := s.pathOf(id)
if !ok {
return nil, errs.New(errs.EditorDocument, "prefab "+id.Short()+" is not in the project")
}
before, err := readFile(s.ctx.Pipeline.Abs(rel))
if err != nil {
return nil, err
}
after := sceneio.EncodePrefab(reg, m)
if string(after) == string(before) {
continue
}
out = append(out, fileChange{id: id, rel: rel, before: before, after: after})
func (s *Service) files(plan *refactor.ApplyPlan) ([]fileChange, error) {
if s.ctx.Pipeline == nil {
return nil, errs.New(errs.EditorCommand, "the asset pipeline is not available")
}
edits, err := plan.Edits(s.ctx.Types.Schema(), s.ctx.Pipeline)
if err != nil {
return nil, err
}
out := make([]fileChange, len(edits))
for i, e := range edits {
out[i] = fileChange{id: e.ID, rel: e.Path, before: e.Before, after: e.After}
}
slices.SortFunc(out, func(a, b fileChange) int { return strings.Compare(a.rel, b.rel) })
return out, nil
}
+2 -1
View File
@@ -3,6 +3,7 @@ package prefabs
import (
"aego/core/ids"
"aego/editor/core/document"
"aego/project/refactor"
"aego/scene"
"aego/scene/prefab"
"aego/scene/sceneio"
@@ -259,5 +260,5 @@ func (s *Service) nearestOf(d *document.SceneDocument, node ids.NodeID) (ids.Ass
if err != nil {
return ids.AssetID{}, false
}
return newBaker(s.lib()).Nearest(pi.Prefab, key)
return refactor.Nearest(s.lib(), pi.Prefab, key)
}
+2 -1
View File
@@ -2,10 +2,11 @@ package prefabs
import (
"aego/core/ids"
"aego/project/refactor"
"aego/scene/prefab"
"aego/scene/sceneio"
)
func NearestForTest(lib *sceneio.Library, id ids.AssetID, key prefab.Key) (ids.AssetID, bool) {
return newBaker(lib).Nearest(id, key)
return refactor.Nearest(lib, id, key)
}
+6 -49
View File
@@ -14,7 +14,9 @@ import (
"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"
)
@@ -197,13 +199,9 @@ func (s *Service) CreateFrom(d *document.SceneDocument, nodes []ids.NodeID, dir
}
tx := d.History.Begin("Create Prefab")
for _, h := range tops {
if pi, ok := sceneio.InstanceOf(sc, h); ok && pi.Owner {
if err := refactor.CheckCreate(sc, h); err != nil {
tx.Abort()
return errs.New(errs.EditorCommand, sc.Name(h)+" is already a prefab instance, use Create Variant")
}
if root, _, ok := sceneio.OwnerRoot(sc, h); ok && root != h {
tx.Abort()
return errs.New(errs.EditorCommand, sc.Name(h)+" belongs to a prefab instance, unpack it first")
return err
}
rel := assetops.UniqueFile(s.ctx, dir, fileSafe(sc.Name(h)), document.PrefabExt)
before := sceneio.SaveSubtree(sc, h, true)
@@ -212,12 +210,12 @@ func (s *Service) CreateFrom(d *document.SceneDocument, nodes []ids.NodeID, dir
tx.Abort()
return err
}
after, err := s.linkBlock(sc, h, id)
after, err := refactor.LinkBlock(sc, h, s.lib(), id)
if err != nil {
tx.Abort()
return err
}
if err := tx.Exec(&commands.Replace{Node: sc.ID(h), Before: before, After: after, Name: "Create Prefab"}); err != nil {
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
}
@@ -227,47 +225,6 @@ func (s *Service) CreateFrom(d *document.SceneDocument, nodes []ids.NodeID, dir
return nil
}
func (s *Service) linkBlock(sc *scene.Scene, h scene.NodeHandle, id ids.AssetID) ([]byte, error) {
lib := s.lib()
p, ok := lib.Prefab(id)
if !ok {
return nil, errs.New(errs.EditorCommand, "the new prefab did not import")
}
tree, _, err := prefab.Expand(lib, p, nil)
if err != nil {
return nil, err
}
inst := &prefab.Instance{Prefab: id, IDs: map[prefab.Key]ids.NodeID{}}
tree.Walk(func(n, _ *prefab.Node) {
if sid, ok := sceneIDOf(sc, n.Key); ok {
inst.IDs[n.Key] = sid
}
})
return sceneio.EncodeInstance(sc.Registry(), sc.ID(h), inst), nil
}
func sceneIDOf(sc *scene.Scene, k prefab.Key) (ids.NodeID, bool) {
head, rest, nested := strings.Cut(string(k), "/")
first, err := ids.ParseNodeID(head)
if err != nil {
return ids.NodeID{}, false
}
if !nested {
return first, !sc.Handle(first).IsZero()
}
if pi, ok := sceneio.InstanceOf(sc, sc.Handle(first)); ok {
for sid, kk := range pi.Keys {
if string(kk) == rest {
return sid, true
}
}
}
if local, ok := k.Local(); ok && !sc.Handle(local).IsZero() {
return local, true
}
return ids.NodeID{}, false
}
func (s *Service) CreateVariant(id ids.AssetID) error {
if err := s.editable(); err != nil {
return err
+9 -6
View File
@@ -6,6 +6,7 @@ import (
"aego/editor/core/history"
"aego/editor/core/history/commands"
"aego/scene"
"aego/scene/mutate"
)
func (a *Access) Rename(tx *history.Transaction, node ids.NodeID, name string) error {
@@ -13,27 +14,29 @@ func (a *Access) Rename(tx *history.Transaction, node ids.NodeID, name string) e
if err != nil {
return err
}
before := a.s.Name(h)
if before == name || name == "" {
if a.s.Name(h) == name || name == "" {
return nil
}
return a.within(tx, "Rename", func(tx *history.Transaction) error {
return tx.Exec(&commands.Rename{Node: node, Before: before, After: name})
return tx.Exec(commands.NewMutation("Rename", commands.EnvFor(a.s), mutate.Rename(commands.Ref(node), name)))
})
}
func (a *Access) SetEnabled(tx *history.Transaction, nodes []ids.NodeID, v bool) error {
text := "false"
if v {
text = "true"
}
return a.within(tx, "Toggle Enabled", func(tx *history.Transaction) error {
for _, n := range nodes {
h, err := a.handle(n)
if err != nil {
return err
}
before := a.s.Enabled(h)
if before == v {
if a.s.Enabled(h) == v {
continue
}
if err := tx.Exec(&commands.SetEnabled{Node: n, Before: before, After: v}); err != nil {
if err := tx.Exec(commands.NewMutation("Toggle Enabled", commands.EnvFor(a.s), mutate.Set(commands.Ref(n), "enabled", text))); err != nil {
return err
}
}
+2 -2
View File
@@ -113,7 +113,7 @@ func (a *Access) within(tx *history.Transaction, label string, fn func(*history.
}
func (a *Access) Set(tx *history.Transaction, nodes []ids.NodeID, t schema.TypeID, f schema.FieldID, v schema.Value) error {
_, fd, err := a.Descriptor(t, f)
td, fd, err := a.Descriptor(t, f)
if err != nil {
return err
}
@@ -131,7 +131,7 @@ func (a *Access) Set(tx *history.Transaction, nodes []ids.NodeID, t schema.TypeI
if Equal(before, nv) {
continue
}
if err := tx.Exec(&commands.SetProperty{Node: n, Type: t, Field: f, Before: before, After: nv, Name: name}); err != nil {
if err := tx.Exec(commands.SetField(a.s, n, td, fd, nv, name)); err != nil {
return err
}
}
+49 -344
View File
File diff suppressed because it is too large Load Diff
-15
View File
@@ -4,7 +4,6 @@ import (
"testing"
"aego/core/ids"
"aego/format/aetext"
"aego/schema"
)
@@ -19,17 +18,3 @@ func TestRewriteNested(t *testing.T) {
t.Fatal("no old reference may remain")
}
}
func TestTextPlaces(t *testing.T) {
id := ids.AssetID{Hi: 7, Lo: 9}
src := "scene version=1 {\n node \"n1\" name=\"Player\" {\n component \"engine.SpriteRenderer\" version=1 {\n sprite = " +
string(token(id)) + "\n }\n }\n}\n"
doc, err := aetext.Parse([]byte(src), "x.aescene")
if err != nil {
t.Fatal(err)
}
got := textPlaces(doc, vAsset, id.String())
if len(got) != 1 || got[0].Path != "Player" || got[0].Owner != "engine.SpriteRenderer" || got[0].Field != "sprite" {
t.Fatalf("places = %+v", got)
}
}
+3 -7
View File
@@ -372,16 +372,12 @@ func InstantiatePlace(ctx *context.Editor, t Template, parent ids.NodeID, index
if err := place(s, scratch, h, parent, pl); err != nil {
return nil, err
}
cmd := &commands.Create{
Parent: parent,
Index: index,
Fragment: sceneio.SaveSubtree(scratch, h, true),
Name: t.Name,
}
cmd := commands.NewMutation("Create "+t.Name, commands.EnvFor(s),
commands.Insert(parent, index, string(sceneio.SaveSubtree(scratch, h, true))))
if err := d.History.Exec(cmd); err != nil {
return nil, err
}
created := append([]ids.NodeID(nil), cmd.Roots()...)
created := cmd.Created()
ctx.Select(created...)
return created, nil
}
+7 -30
View File
@@ -11,9 +11,10 @@ import (
"aego/editor/core/assetops"
"aego/editor/core/context"
"aego/editor/core/document"
"aego/editor/core/rename"
"aego/editor/core/services"
"aego/platform/input"
"aego/project/refactor"
"aego/project/rename"
"aego/ui/core"
"aego/ui/widgets"
)
@@ -300,34 +301,10 @@ func (g *Dialog) applyNodes() error {
}
func (g *Dialog) applyAssets() error {
finals := map[string]bool{}
sources := map[string]bool{}
var ops []assetops.FileOp
for i, rel := range g.paths {
rw := g.rows[i]
if !rw.Changed() {
continue
}
to := path.Join(path.Dir(rel), rw.To+path.Ext(rel))
finals[strings.ToLower(to)] = true
sources[strings.ToLower(rel)] = true
ops = append(ops, assetops.FileOp{ID: g.assets[i], From: rel, To: to})
ops := refactor.RenameOps(g.paths, g.assets, g.rows)
list := make([]assetops.FileOp, len(ops))
for i, op := range ops {
list[i] = assetops.FileOp{ID: op.ID, From: op.From, To: op.To}
}
chain := false
for to := range finals {
if sources[to] {
chain = true
break
}
}
if chain {
var temp, final []assetops.FileOp
for i, op := range ops {
tmp := path.Join(path.Dir(op.From), "__rename_"+strconv.Itoa(i)+"_"+path.Base(op.From))
temp = append(temp, assetops.FileOp{ID: op.ID, From: op.From, To: tmp})
final = append(final, assetops.FileOp{ID: op.ID, From: tmp, To: op.To})
}
ops = append(temp, final...)
}
return g.d.Move(ops)
return g.d.Move(list)
}
+13 -5
View File
@@ -236,11 +236,19 @@ func (p *Panel) act(a int, ro bool) {
}
switch a {
case actAdd:
i := t.AddRow()
i, err := t.AddRow()
if err != nil {
p.notify(err)
return
}
p.filter = ""
p.Select(t.Rows[i].ID)
case actDuplicate:
made := t.Duplicate(p.selected())
made, err := t.Duplicate(p.selected())
if err != nil {
p.notify(err)
return
}
ids := make([]string, len(made))
for k, i := range made {
ids[k] = t.Rows[i].ID
@@ -249,12 +257,12 @@ func (p *Panel) act(a int, ro bool) {
p.Select(ids...)
}
case actDelete:
t.Delete(p.selected())
p.notify(t.Delete(p.selected()))
clear(p.sel)
case actUndo:
t.Undo()
p.notify(t.Undo())
case actRedo:
t.Redo()
p.notify(t.Redo())
case actExport:
if p.d.Data == nil {
return
+9 -13
View File
@@ -681,33 +681,29 @@ func (p *Panel) move(d *document.SceneDocument, nodes []ids.NodeID, target ids.N
if n == th {
continue
}
cmd := &commands.Reparent{Node: s.ID(n), KeepWorld: true}
parent, index := target, -1
switch zone {
case zoneInto:
cmd.NewParent = target
cmd.NewIndex = -1
case zoneBefore:
parent := s.Parent(th)
ph := s.Parent(th)
idx := commands.ChildIndex(s, th)
if s.Parent(n) == parent && commands.ChildIndex(s, n) < idx {
if s.Parent(n) == ph && commands.ChildIndex(s, n) < idx {
idx--
}
cmd.NewParent = s.ID(parent)
cmd.NewIndex = idx
parent, index = s.ID(ph), idx
case zoneAfter:
parent := s.Parent(anchor)
ph := s.Parent(anchor)
idx := commands.ChildIndex(s, anchor) + 1
if s.Parent(n) == parent && commands.ChildIndex(s, n) < idx {
if s.Parent(n) == ph && commands.ChildIndex(s, n) < idx {
idx--
}
cmd.NewParent = s.ID(parent)
cmd.NewIndex = idx
parent, index = s.ID(ph), idx
}
if p.d.Prefabs != nil && cmd.NewParent != s.ID(s.Parent(n)) && p.d.Prefabs.Locked(d, s.ID(n)) {
if p.d.Prefabs != nil && parent != s.ID(s.Parent(n)) && p.d.Prefabs.Locked(d, s.ID(n)) {
tx.Abort()
p.d.Notices.Notify(context.LevelWarn, s.Name(n)+" comes from a prefab and keeps its parent, unpack the instance to move it")
return
}
cmd := commands.NewMutation("Reparent", commands.EnvFor(s), commands.Move(s.ID(n), parent, index, true))
if err := tx.Exec(cmd); err != nil {
tx.Abort()
p.d.Notices.Notify(context.LevelError, err.Error())
+10 -4
View File
@@ -647,9 +647,10 @@ func (p *Panel) component(d *document.SceneDocument, t schema.TypeID) {
func (p *Panel) remove(d *document.SceneDocument, td *schema.TypeDescriptor) {
p.confirm(d, "Remove "+p.reg.Meta(td).DisplayName, func() error {
env := commands.EnvFor(d.Scene())
tx := d.History.Begin("Remove " + td.FullName)
for _, id := range p.nodes {
if err := tx.Exec(&commands.RemoveComponent{Node: id, Type: td.ID, Name: td.FullName}); err != nil {
if err := tx.Exec(commands.NewMutation("Remove "+td.FullName, env, commands.RemoveComp(id, td))); err != nil {
tx.Abort()
return err
}
@@ -661,13 +662,16 @@ func (p *Panel) remove(d *document.SceneDocument, td *schema.TypeDescriptor) {
func (p *Panel) reset(d *document.SceneDocument, td *schema.TypeDescriptor) {
p.confirm(d, "Reset "+p.reg.Meta(td).DisplayName, func() error {
s := d.Scene()
env := commands.EnvFor(s)
defaults := p.reg.Defaults(td.ID)
tx := d.History.Begin("Reset " + td.FullName)
for _, id := range p.nodes {
if err := tx.Exec(&commands.RemoveComponent{Node: id, Type: td.ID, Name: td.FullName}); err != nil {
if err := tx.Exec(commands.NewMutation("Remove "+td.FullName, env, commands.RemoveComp(id, td))); err != nil {
tx.Abort()
return err
}
if err := tx.Exec(&commands.AddComponent{Node: id, Type: td.ID, Name: td.FullName, Values: p.reg.Defaults(td.ID)}); err != nil {
if err := tx.Exec(commands.NewMutation("Add "+td.FullName, env, commands.AddComp(s, id, td, defaults))); err != nil {
tx.Abort()
return err
}
@@ -796,12 +800,14 @@ func (p *Panel) addComponent(d *document.SceneDocument) {
func (p *Panel) add(d *document.SceneDocument, td *schema.TypeDescriptor) {
p.confirm(d, "Add "+p.reg.Meta(td).DisplayName, func() error {
s := d.Scene()
env := commands.WithOnAdd(commands.EnvFor(s), p.d.Clip.OnAdd)
defaults := p.reg.Defaults(td.ID)
tx := d.History.Begin("Add " + td.FullName)
for _, n := range p.nodes {
if _, ok := s.GetByType(s.Handle(n), td.ID); ok {
continue
}
if err := tx.Exec(&commands.AddComponent{Node: n, Type: td.ID, Name: td.FullName, Values: p.reg.Defaults(td.ID), OnAdd: p.d.Clip.OnAdd(td)}); err != nil {
if err := tx.Exec(commands.NewMutation("Add "+td.FullName, env, commands.AddComp(s, n, td, defaults))); err != nil {
tx.Abort()
return err
}
+37
View File
@@ -0,0 +1,37 @@
package project
import (
"os"
"path/filepath"
)
func WriteAtomic(abs string, data []byte) error {
dir := filepath.Dir(abs)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(dir, ".aego-*.tmp")
if err != nil {
return err
}
name := tmp.Name()
if _, err := tmp.Write(data); err != nil {
tmp.Close()
os.Remove(name)
return err
}
if err := tmp.Chmod(0o644); err != nil {
tmp.Close()
os.Remove(name)
return err
}
if err := tmp.Close(); err != nil {
os.Remove(name)
return err
}
if err := os.Rename(name, abs); err != nil {
os.Remove(name)
return err
}
return nil
}
+1 -1
View File
@@ -282,7 +282,7 @@ func (p *Project) Encode() []byte {
}
func (p *Project) Save() error {
if err := os.WriteFile(p.FilePath(), p.Encode(), 0o644); err != nil {
if err := WriteAtomic(p.FilePath(), p.Encode()); err != nil {
return errs.Wrap(err, errs.VFSIO, "cannot write project.aego")
}
return nil
@@ -1,4 +1,4 @@
package prefabs
package refactor
import (
"slices"
@@ -309,3 +309,7 @@ func (b *baker) Nearest(id ids.AssetID, key prefab.Key) (ids.AssetID, bool) {
}
return d.owner, true
}
func Nearest(lib *sceneio.Library, id ids.AssetID, key prefab.Key) (ids.AssetID, bool) {
return newBaker(lib).Nearest(id, key)
}
+46
View File
@@ -0,0 +1,46 @@
package refactor
import (
"aego/asset/pipeline"
"aego/core/ids"
"aego/project"
"aego/project/refs"
)
func Users(p *project.Project, src refs.Source, list []ids.AssetID) []refs.Ref {
gone := make(map[ids.AssetID]bool, len(list))
for _, id := range list {
gone[id] = true
}
seen := map[string]bool{}
var out []refs.Ref
for _, id := range list {
for _, r := range refs.Referrers(p, src, id) {
if (r.Project || !gone[r.Asset]) && !seen[r.Path] {
seen[r.Path] = true
out = append(out, r)
}
}
}
refs.Sort(out)
return out
}
func Delete(pl *pipeline.Pipeline, list []ids.AssetID) (*pipeline.Batch, error) {
all := &pipeline.Batch{Failed: map[ids.AssetID]error{}}
for _, id := range list {
b, err := pl.Delete(id)
if b != nil {
all.Imported = append(all.Imported, b.Imported...)
all.Removed = append(all.Removed, b.Removed...)
all.Retry = append(all.Retry, b.Retry...)
for k, v := range b.Failed {
all.Failed[k] = v
}
}
if err != nil {
return all, err
}
}
return all, nil
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
package refs
import (
"testing"
"aego/core/ids"
"aego/format/aetext"
)
func TestTextPlaces(t *testing.T) {
id := ids.AssetID{Hi: 7, Lo: 9}
src := "scene version=1 {\n node \"n1\" name=\"Player\" {\n component \"engine.SpriteRenderer\" version=1 {\n sprite = " +
string(Token(id)) + "\n }\n }\n}\n"
doc, err := aetext.Parse([]byte(src), "x.aescene")
if err != nil {
t.Fatal(err)
}
got := textPlaces(doc, vAsset, id.String())
if len(got) != 1 || got[0].Path != "Player" || got[0].Owner != "engine.SpriteRenderer" || got[0].Field != "sprite" {
t.Fatalf("places = %+v", got)
}
}

Some files were not shown because too many files have changed in this diff Show More