Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a22396467 | ||
|
|
21e03128a8 | ||
|
|
e5b1964f3e | ||
|
|
cffa66514b | ||
|
|
2744cff481 | ||
|
|
91052a54db | ||
|
|
7f4d87ef2c |
+91
-1
File diff suppressed because one or more lines are too long
@@ -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:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mustFind(t *testing.T, name string) *Command {
|
||||
t.Helper()
|
||||
c, ok := Find(name)
|
||||
if !ok {
|
||||
t.Fatalf("no command %s", name)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func TestParseArgs(t *testing.T) {
|
||||
set := mustFind(t, "scene.set")
|
||||
a, f, err := parseArgs(set, []string{"s.aescene", "--json", "Player", "--dry-run", "Body2D.mass", "-3"})
|
||||
if err != nil || !f.json || a.Str("value") != "-3" || a.Str("node") != "Player" || !a.Bool("dry-run") {
|
||||
t.Fatalf("flags after positionals: %v %+v %v", a, f, err)
|
||||
}
|
||||
add := mustFind(t, "scene.add-node")
|
||||
for _, toks := range [][]string{{"s", "N", "--index=1"}, {"s", "N", "--index", "1"}} {
|
||||
a, _, err := parseArgs(add, toks)
|
||||
if n, _ := a.Int("index", -1); err != nil || n != 1 {
|
||||
t.Fatalf("%v: %v %v", toks, a, err)
|
||||
}
|
||||
}
|
||||
a, _, err = parseArgs(set, []string{"s", "--", "Player", "Body2D.mass", "--odd"})
|
||||
if err != nil || a.Str("value") != "--odd" {
|
||||
t.Fatalf("after --: %v %v", a, err)
|
||||
}
|
||||
if _, _, err := parseArgs(set, []string{"s", "--nope"}); err == nil {
|
||||
t.Fatal("an unknown flag was accepted")
|
||||
}
|
||||
if _, _, err := parseArgs(set, []string{"s", "a", "b", "c", "d"}); err == nil {
|
||||
t.Fatal("an extra positional was accepted")
|
||||
}
|
||||
var out, errw bytes.Buffer
|
||||
if code := Main(nil, []string{"scene", "set", "s", "--nope"}, &out, &errw, Options{}); code != 2 {
|
||||
t.Fatalf("unknown flag exit %d", code)
|
||||
}
|
||||
if code := Main(nil, []string{"scene", "set", "a", "b", "c", "d", "e"}, &out, &errw, Options{}); code != 2 {
|
||||
t.Fatalf("extra positional exit %d", code)
|
||||
}
|
||||
if code := Main(nil, []string{"scene", "help"}, &out, &errw, Options{}); code != 0 {
|
||||
t.Fatalf("help exit %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemasAndToolNames(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
for _, c := range Commands() {
|
||||
if seen[c.Tool()] {
|
||||
t.Fatalf("tool name %s is used twice", c.Tool())
|
||||
}
|
||||
seen[c.Tool()] = true
|
||||
b, err := json.Marshal(c.Schema())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var sch struct {
|
||||
Type string `json:"type"`
|
||||
Properties map[string]map[string]any `json:"properties"`
|
||||
Required []string `json:"required"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &sch); err != nil || sch.Type != "object" {
|
||||
t.Fatalf("%s: %s", c.Name, b)
|
||||
}
|
||||
var want []string
|
||||
for _, x := range c.AllArgs() {
|
||||
if x.Required {
|
||||
want = append(want, x.Name)
|
||||
}
|
||||
if _, ok := sch.Properties[x.Name]; !ok {
|
||||
t.Fatalf("%s: no property %s", c.Name, x.Name)
|
||||
}
|
||||
}
|
||||
if len(want) != len(sch.Required) {
|
||||
t.Fatalf("%s: required %v, want %v", c.Name, sch.Required, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
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
@@ -0,0 +1,66 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"aego/asset"
|
||||
"aego/project/refs"
|
||||
"aego/scene/mutate"
|
||||
)
|
||||
|
||||
func projectCommands() []*Command {
|
||||
return []*Command{
|
||||
{Name: "project.validate", Doc: "missing assets and nodes, import errors, prefabs and scenes that do not load", Run: (*Session).projectValidate},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) projectValidate(Args) (*Output, error) {
|
||||
out := &Output{}
|
||||
add := func(err bool, node, msg string) {
|
||||
out.Problems = append(out.Problems, mutate.Problem{Error: err, Node: node, Message: msg})
|
||||
}
|
||||
for _, m := range refs.MissingAssets(s.Project, s.Pipeline) {
|
||||
add(true, "", "missing asset "+m.Label())
|
||||
}
|
||||
for _, m := range refs.MissingNodes(s.Pipeline) {
|
||||
add(true, m.Scene+" "+m.Path, m.Owner+"."+m.Field+" references a node that is not in the scene")
|
||||
}
|
||||
if s.scriptErr != nil {
|
||||
add(true, "", "script types: "+s.scriptErr.Error())
|
||||
}
|
||||
for _, e := range s.libErrs {
|
||||
add(true, "", e.Error())
|
||||
}
|
||||
for _, x := range s.Pipeline.Assets() {
|
||||
if x.Error != "" {
|
||||
add(true, x.Path, x.Error)
|
||||
continue
|
||||
}
|
||||
if x.Kind != asset.ArtScene && x.Kind != asset.ArtPrefab {
|
||||
continue
|
||||
}
|
||||
src, err := s.read(x.Path)
|
||||
if err != nil {
|
||||
add(true, x.Path, err.Error())
|
||||
continue
|
||||
}
|
||||
doc, err := mutate.Load(src, x.Path, s.env())
|
||||
if err != nil {
|
||||
add(true, x.Path, err.Error())
|
||||
continue
|
||||
}
|
||||
for _, p := range doc.Problems {
|
||||
add(p.Error, x.Path+" "+p.Node, p.Message)
|
||||
}
|
||||
}
|
||||
errs, warns := 0, 0
|
||||
for _, p := range out.Problems {
|
||||
if p.Error {
|
||||
errs++
|
||||
} else {
|
||||
warns++
|
||||
}
|
||||
}
|
||||
out.Notes = []string{"project: " + strconv.Itoa(errs) + " error(s), " + strconv.Itoa(warns) + " warning(s)"}
|
||||
return out, 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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"aego/app/agent"
|
||||
"aego/core/log"
|
||||
"aego/core/rewind"
|
||||
"aego/project"
|
||||
"aego/sdk"
|
||||
)
|
||||
|
||||
const maxRunLog = 200
|
||||
|
||||
func AgentRunner(mods ...sdk.Module) agent.Runner {
|
||||
return func(p *project.Project, r agent.RunRequest) (agent.RunReport, error) {
|
||||
var buf bytes.Buffer
|
||||
lg := log.New(log.NewTextSink(&buf))
|
||||
lg.SetLevel(log.Warn)
|
||||
frames, seed := r.Frames, r.Seed
|
||||
if frames <= 0 {
|
||||
frames = 120
|
||||
}
|
||||
if seed == 0 {
|
||||
seed = 1
|
||||
}
|
||||
a, err := Open(p, Config{Headless: true, FrameTime: defaultDetStep, Seed: seed, ScenePath: r.Scene, Logger: lg, NoProfile: true}, mods...)
|
||||
if err != nil {
|
||||
return agent.RunReport{Log: logLines(buf.String())}, err
|
||||
}
|
||||
rep := agent.RunReport{}
|
||||
driver := a.Driver()
|
||||
for rep.Frames < frames {
|
||||
if !a.Engine.Step(driver) {
|
||||
rep.Error = "the game stopped at frame " + strconv.Itoa(rep.Frames+1)
|
||||
if err := a.Engine.Err(); err != nil {
|
||||
rep.Error += ": " + err.Error()
|
||||
}
|
||||
break
|
||||
}
|
||||
rep.Frames++
|
||||
}
|
||||
sum, _, _ := WorldHash(a.Engine.MainWorld(), rewind.NewHasher())
|
||||
rep.Hash = hex.EncodeToString(sum[:])
|
||||
a.Close()
|
||||
rep.Log = logLines(buf.String())
|
||||
return rep, nil
|
||||
}
|
||||
}
|
||||
|
||||
func logLines(s string) []string {
|
||||
var out []string
|
||||
for _, l := range strings.Split(strings.TrimSpace(s), "\n") {
|
||||
if l = strings.TrimSpace(l); l != "" {
|
||||
out = append(out, l)
|
||||
}
|
||||
}
|
||||
if len(out) > maxRunLog {
|
||||
out = append(out[:maxRunLog], "…")
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func WorldHash(w *engine.World, h *rewind.Hasher) (sum [32]byte, covered, skippe
|
||||
}
|
||||
|
||||
func determinismPass(p *project.Project, o DeterminismOptions, mods []sdk.Module) ([]checkpoint, []string, []string, error) {
|
||||
a, err := Open(p, Config{Headless: true, FrameTime: o.FrameTime, Seed: o.Seed, ScenePath: o.ScenePath, Logger: log.Nop()}, mods...)
|
||||
a, err := Open(p, Config{Headless: true, FrameTime: o.FrameTime, Seed: o.Seed, ScenePath: o.ScenePath, Logger: log.Nop(), NoProfile: true}, mods...)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ type Config struct {
|
||||
Profile string
|
||||
Logger *log.Logger
|
||||
Seed uint64
|
||||
NoProfile bool
|
||||
}
|
||||
|
||||
type App struct {
|
||||
@@ -138,6 +139,7 @@ func Open(p *project.Project, cfg Config, mods ...sdk.Module) (*App, error) {
|
||||
Debug: cfg.Debug,
|
||||
FailOnScriptErrors: true,
|
||||
Profile: cfg.Profile,
|
||||
NoProfile: cfg.NoProfile,
|
||||
}, sdk.AdaptAll(mods)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"aego/app/agent"
|
||||
"aego/app/agent/mcp"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -39,6 +42,10 @@ func Tool(p *project.Project, args []string, stdout, stderr io.Writer, mods ...s
|
||||
return toolTest(p, args[1:], stdout, stderr, mods)
|
||||
case "docs":
|
||||
return docstool.Tool(p, args[1:], stdout, stderr, sdk.AdaptAll(mods)...)
|
||||
case "agent":
|
||||
return agent.Main(p, args[1:], stdout, stderr, agent.Options{Mods: mods, Run: AgentRunner(mods...)})
|
||||
case "mcp":
|
||||
return mcp.Main(p, args[1:], os.Stdin, stdout, stderr, agent.Options{Mods: mods, Run: AgentRunner(mods...)})
|
||||
}
|
||||
fmt.Fprintf(stderr, "unknown tool %q\n", args[0])
|
||||
return 2
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"aego/app/agent"
|
||||
"aego/app/agent/mcp"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"aego/app/bootstrap"
|
||||
@@ -50,6 +53,8 @@ func dispatch(args []string) int {
|
||||
return cmdCheck(args[1:])
|
||||
case "icons":
|
||||
return cmdIcons(args[1:])
|
||||
case "mcp":
|
||||
return cmdMcp(args[1:])
|
||||
case "docs":
|
||||
return cmdDocs(args[1:])
|
||||
case "test":
|
||||
@@ -60,6 +65,9 @@ func dispatch(args []string) int {
|
||||
usage()
|
||||
return 0
|
||||
}
|
||||
if slices.Contains(agent.Groups(), args[0]) {
|
||||
return cmdAgent(args)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "unknown command %q\n", args[0])
|
||||
usage()
|
||||
return 2
|
||||
@@ -83,6 +91,8 @@ func usage() {
|
||||
run *_test.aes script tests headless, or run the game twice and compare state hashes
|
||||
dump script <pkg|Type> [--func name] disassemble a script package with source lines
|
||||
docs export|check|build|serve documentation registry, site and playground
|
||||
mcp [--project dir] [--watch] serve the project to an AI agent over MCP (stdio)
|
||||
scene|prefab|data|refs|asset|schema|project <verb> ... edit project files without the editor, see aego scene help
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -97,6 +107,40 @@ func openProject(dir string) (*project.Project, error) {
|
||||
return project.Load(root)
|
||||
}
|
||||
|
||||
func projectFlag(args []string) string {
|
||||
for i, a := range args {
|
||||
switch {
|
||||
case a == "--project" && i+1 < len(args):
|
||||
return args[i+1]
|
||||
case strings.HasPrefix(a, "--project="):
|
||||
return strings.TrimPrefix(a, "--project=")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func cmdAgent(args []string) int {
|
||||
p, err := openProject(projectFlag(args))
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if bootstrap.NeedsNative(p) {
|
||||
return scriptTool(p, append([]string{"agent"}, args...)...)
|
||||
}
|
||||
return agent.Main(p, args, os.Stdout, os.Stderr, agent.Options{Run: run.AgentRunner()})
|
||||
}
|
||||
|
||||
func cmdMcp(args []string) int {
|
||||
p, err := openProject(projectFlag(args))
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
}
|
||||
if bootstrap.NeedsNative(p) {
|
||||
return scriptTool(p, append([]string{"mcp"}, args...)...)
|
||||
}
|
||||
return mcp.Main(p, args, os.Stdin, os.Stdout, os.Stderr, agent.Options{Run: run.AgentRunner()})
|
||||
}
|
||||
|
||||
func firstArg(args []string) (string, []string) {
|
||||
if len(args) > 0 && len(args[0]) > 0 && args[0][0] != '-' {
|
||||
return args[0], args[1:]
|
||||
|
||||
@@ -23,6 +23,7 @@ func scriptTool(p *project.Project, args ...string) int {
|
||||
}
|
||||
cmd := exec.Command(binary, append([]string{"--tool"}, args...)...)
|
||||
cmd.Dir = p.Root
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return exitCode(cmd.Run())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -160,26 +160,27 @@ type ScriptHost interface {
|
||||
}
|
||||
|
||||
type Editor struct {
|
||||
Engine *engine.Engine
|
||||
Renderer *render.Renderer
|
||||
Project *project.Project
|
||||
Pipeline *pipeline.Pipeline
|
||||
Loader *loader.Loader
|
||||
Types *scene.Types
|
||||
Docs *document.Manager
|
||||
Selection *selection.Service
|
||||
Commands *command.Registry
|
||||
UI *core.Context
|
||||
DnD *dnd.State
|
||||
Clipboard DataClipboard
|
||||
Log *log.ChannelLogger
|
||||
Settings SceneSettings
|
||||
Thumbs ThumbSource
|
||||
Scripts ScriptHost
|
||||
Code ScriptNav
|
||||
Shapes *shape.Registry
|
||||
Problems *problems.Service
|
||||
Sections *module.Sections
|
||||
BeforeSwap func(*pipeline.Batch) func()
|
||||
Engine *engine.Engine
|
||||
Renderer *render.Renderer
|
||||
Project *project.Project
|
||||
Pipeline *pipeline.Pipeline
|
||||
Loader *loader.Loader
|
||||
Types *scene.Types
|
||||
Docs *document.Manager
|
||||
Selection *selection.Service
|
||||
Commands *command.Registry
|
||||
UI *core.Context
|
||||
DnD *dnd.State
|
||||
Clipboard DataClipboard
|
||||
Log *log.ChannelLogger
|
||||
Settings SceneSettings
|
||||
Thumbs ThumbSource
|
||||
Scripts ScriptHost
|
||||
Code ScriptNav
|
||||
Shapes *shape.Registry
|
||||
Problems *problems.Service
|
||||
Sections *module.Sections
|
||||
|
||||
ShowProblems func(filter string)
|
||||
RevealPath func(rel string)
|
||||
@@ -428,6 +429,10 @@ func (e *Editor) ApplyBatch(b *pipeline.Batch) {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
var after func()
|
||||
if e.BeforeSwap != nil {
|
||||
after = e.BeforeSwap(b)
|
||||
}
|
||||
if e.Loader != nil && e.Pipeline != nil {
|
||||
if len(b.Imported) > 0 {
|
||||
if err := e.Loader.Swap(b.Imported, e.Pipeline.Artifacts()); err != nil {
|
||||
@@ -438,6 +443,9 @@ func (e *Editor) ApplyBatch(b *pipeline.Batch) {
|
||||
e.Loader.Drop(id)
|
||||
}
|
||||
}
|
||||
if after != nil {
|
||||
after()
|
||||
}
|
||||
for id, err := range b.Failed {
|
||||
e.Notify(LevelWarn, "import failed for "+id.String()[:8]+": "+err.Error())
|
||||
}
|
||||
|
||||
+121
-109
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
@@ -117,7 +130,12 @@ func (s *Service) Doc(id ids.AssetID) (*Doc, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d != nil && (d.Dirty() || bytes.Equal(raw, d.disk)) {
|
||||
if d != nil && d.Dirty() {
|
||||
s.conflict(rel, raw, d.disk, &d.conflict)
|
||||
d.Path, d.rev = rel, s.ctx.AssetRev
|
||||
return d, d.load()
|
||||
}
|
||||
if d != nil && bytes.Equal(raw, d.disk) {
|
||||
d.Path, d.rev = rel, s.ctx.AssetRev
|
||||
return d, d.load()
|
||||
}
|
||||
@@ -125,11 +143,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
|
||||
}
|
||||
@@ -140,22 +154,21 @@ func (s *Service) Doc(id ids.AssetID) (*Doc, error) {
|
||||
|
||||
func (s *Service) Table(id ids.AssetID) (*Table, error) {
|
||||
t := s.tables[id]
|
||||
if t != nil && (t.rev == s.ctx.AssetRev || t.Dirty()) {
|
||||
if t != nil && t.rev == s.ctx.AssetRev {
|
||||
return t, nil
|
||||
}
|
||||
rel, raw, err := s.file(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t != nil && bytes.Equal(raw, t.disk) {
|
||||
if t != nil && (t.Dirty() || bytes.Equal(raw, t.disk)) {
|
||||
if t.Dirty() {
|
||||
s.conflict(rel, raw, t.disk, &t.conflict)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -164,6 +177,14 @@ func (s *Service) Table(id ids.AssetID) (*Table, error) {
|
||||
return nt, nil
|
||||
}
|
||||
|
||||
func (s *Service) conflict(rel string, raw, disk []byte, seen *[]byte) {
|
||||
if bytes.Equal(raw, disk) || bytes.Equal(raw, *seen) {
|
||||
return
|
||||
}
|
||||
*seen = raw
|
||||
s.ctx.Notify(context.LevelWarn, rel+" changed on disk while it has unsaved changes: Save keeps yours, Revert loads the disk version")
|
||||
}
|
||||
|
||||
func (s *Service) RevertDoc(id ids.AssetID) {
|
||||
delete(s.docs, id)
|
||||
}
|
||||
|
||||
+138
-132
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
+36
-202
File diff suppressed because it is too large
Load Diff
@@ -3,14 +3,16 @@ package prefabs
|
||||
import (
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"aego/asset"
|
||||
"aego/asset/pipeline"
|
||||
"aego/core/errs"
|
||||
"aego/core/ids"
|
||||
"aego/editor/core/context"
|
||||
"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 +82,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
|
||||
}
|
||||
|
||||
@@ -106,6 +101,8 @@ func (s *Service) reimport(list []ids.AssetID) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
s.writing = true
|
||||
defer func() { s.writing = false }()
|
||||
batch, err := s.ctx.Pipeline.Reimport(list...)
|
||||
s.ctx.ApplyBatch(batch)
|
||||
if err != nil {
|
||||
@@ -298,3 +295,31 @@ func (s *Service) commit(label string, files []fileChange, src *source) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) External(b *pipeline.Batch) func() {
|
||||
if s.writing || s.ctx.Docs.InPlay() {
|
||||
return nil
|
||||
}
|
||||
changed := map[ids.AssetID]bool{}
|
||||
for _, e := range b.Imported {
|
||||
if e.Kind == asset.ArtPrefab {
|
||||
changed[e.ID] = true
|
||||
}
|
||||
}
|
||||
if len(changed) == 0 {
|
||||
return nil
|
||||
}
|
||||
plan := s.affected(s.lib(), changed, true)
|
||||
if len(plan) == 0 {
|
||||
return nil
|
||||
}
|
||||
return func() {
|
||||
lib := s.lib()
|
||||
for _, t := range plan {
|
||||
if _, err := commands.ReplaceNode(t.doc.Scene(), t.node, t.parent, t.index, t.block, lib); err != nil {
|
||||
s.ctx.Notify(context.LevelError, "cannot refresh the prefab instances in "+t.doc.Path+": "+err.Error())
|
||||
}
|
||||
}
|
||||
clear(s.infos)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user