ci / checks (push) Successful in 5m14s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m39s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / release (push) Successful in 5m44s
The agent attaches and removes scripts with add-comp and remove-comp by the script's type name, reads and sets their exported fields like component fields with short names (EnemyAI.PointA), and node reference fields take a node path. scene_get lists a node's script fields. scene_run works inside the editor: the game runs headless in a child process of the editor's own binary (--tool agent-run), with the open scene as the editor has it, unsaved changes included; scripts with unsaved changes are named in the answer. watch samples the world positions of nodes through the run, so the agent can check that something moves. Following brings the Scene View back in front of the code editor on a scene step and shows the Inspector, and a scene_apply leads to the node its transaction names.
758 lines
20 KiB
Go
758 lines
20 KiB
Go
package mutate
|
|
|
|
import (
|
|
"errors"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"aego/core/errs"
|
|
"aego/core/ids"
|
|
"aego/core/log"
|
|
"aego/format/aetext"
|
|
"aego/scene"
|
|
"aego/scene/sceneio"
|
|
"aego/scene/scriptcomp"
|
|
"aego/schema"
|
|
)
|
|
|
|
var ErrNothing = errors.New("nothing matches")
|
|
|
|
type Change struct {
|
|
Op string `json:"op"`
|
|
Node string `json:"node"`
|
|
Path string `json:"path,omitempty"`
|
|
Before string `json:"before,omitempty"`
|
|
After string `json:"after,omitempty"`
|
|
NodeID ids.NodeID `json:"-"`
|
|
Type schema.TypeID `json:"-"`
|
|
Field schema.FieldID `json:"-"`
|
|
}
|
|
|
|
type Result struct {
|
|
Inverse Tx `json:"-"`
|
|
Redo Tx `json:"-"`
|
|
Changes []Change `json:"changes"`
|
|
Problems []Problem `json:"problems"`
|
|
Created []string `json:"created,omitempty"`
|
|
DryRun bool `json:"dryRun,omitempty"`
|
|
}
|
|
|
|
type OpError struct {
|
|
Index int
|
|
Kind string
|
|
Err error
|
|
}
|
|
|
|
func (e *OpError) Error() string {
|
|
return "operation " + strconv.Itoa(e.Index+1) + " (" + e.Kind + "): " + e.Err.Error()
|
|
}
|
|
|
|
func (e *OpError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
type step struct {
|
|
inv Op
|
|
redo Op
|
|
change Change
|
|
}
|
|
|
|
type doer interface {
|
|
do(op Op, res *Result) (step, error)
|
|
}
|
|
|
|
func run(d doer, tx Tx) (*Result, error) {
|
|
res := &Result{Inverse: Tx{Label: tx.Label}, Redo: Tx{Label: tx.Label}}
|
|
var inv []Op
|
|
for i, op := range tx.Ops {
|
|
st, err := d.do(op, res)
|
|
if err != nil {
|
|
slices.Reverse(inv)
|
|
replay(d, inv)
|
|
return nil, &OpError{Index: i, Kind: op.Kind, Err: err}
|
|
}
|
|
inv = append(inv, st.inv)
|
|
res.Redo.Ops = append(res.Redo.Ops, st.redo)
|
|
res.Changes = append(res.Changes, st.change)
|
|
}
|
|
slices.Reverse(inv)
|
|
res.Inverse.Ops = inv
|
|
return res, nil
|
|
}
|
|
|
|
func replay(d doer, ops []Op) {
|
|
var scratch Result
|
|
for _, op := range ops {
|
|
d.do(op, &scratch)
|
|
}
|
|
}
|
|
|
|
func dryRun(d doer, tx Tx) (*Result, error) {
|
|
res, err := run(d, tx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
replay(d, res.Inverse.Ops)
|
|
res.Inverse, res.Redo, res.DryRun = Tx{}, Tx{}, true
|
|
return res, nil
|
|
}
|
|
|
|
func (d *Document) Apply(tx Tx) (*Result, error) {
|
|
return run(d, tx)
|
|
}
|
|
|
|
func (d *Document) DryRun(tx Tx) (*Result, error) {
|
|
return dryRun(d, tx)
|
|
}
|
|
|
|
func (d *Document) do(op Op, res *Result) (step, error) {
|
|
switch op.Kind {
|
|
case KindAddNode:
|
|
return d.addNode(op, res)
|
|
case KindRemoveNode:
|
|
return d.removeNode(op)
|
|
case KindRestoreNode, KindInsert:
|
|
return d.insert(op, res)
|
|
case KindReplace:
|
|
h, err := d.node(op.Node)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
st, _, err := d.replaceWith(h, []byte(op.Fragment), op.Kind)
|
|
return st, err
|
|
case KindMove:
|
|
return d.move(op)
|
|
case KindRename:
|
|
return d.rename(op)
|
|
case KindAddComp:
|
|
return d.addComp(op, res)
|
|
case KindRemoveComp:
|
|
return d.removeComp(op, res)
|
|
case KindSet:
|
|
v, err := op.value()
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
return d.set(op, v, false)
|
|
case KindReset:
|
|
return d.set(op, aetext.Value{}, true)
|
|
case KindPrefabInstantiate:
|
|
return d.instantiate(op, res)
|
|
case KindPrefabRevert:
|
|
return d.revertPrefab(op)
|
|
case KindPrefabUnpack:
|
|
return d.unpack(op)
|
|
}
|
|
if dataKinds[op.Kind] {
|
|
return step{}, errs.New(errs.EditorCommand, op.Kind+" applies to data assets, not scenes")
|
|
}
|
|
return step{}, errs.New(errs.EditorCommand, "unknown operation "+strconv.Quote(op.Kind))
|
|
}
|
|
|
|
func childIndex(s *scene.Scene, h scene.NodeHandle) int {
|
|
i, idx := 0, -1
|
|
s.Children(s.Parent(h), func(c scene.NodeHandle) bool {
|
|
if c == h {
|
|
idx = i
|
|
return false
|
|
}
|
|
i++
|
|
return true
|
|
})
|
|
return idx
|
|
}
|
|
|
|
func (d *Document) place(h, parent scene.NodeHandle, op Op) error {
|
|
if !op.HasIndex {
|
|
return nil
|
|
}
|
|
if err := d.Scene.Reparent(h, parent, op.Index); err != nil {
|
|
return err
|
|
}
|
|
d.Scene.Flush()
|
|
return nil
|
|
}
|
|
|
|
func (d *Document) change(kind string, h scene.NodeHandle) Change {
|
|
return Change{Op: kind, Node: d.nodeName(h), NodeID: d.Scene.ID(h)}
|
|
}
|
|
|
|
func (d *Document) addNode(op Op, res *Result) (step, error) {
|
|
s := d.Scene
|
|
p, err := d.node(op.Parent)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
if op.Name == "" {
|
|
return step{}, errs.New(errs.EditorCommand, "add-node needs a name")
|
|
}
|
|
var h scene.NodeHandle
|
|
switch {
|
|
case op.ID != "":
|
|
id, err := ids.ParseNodeID(op.ID)
|
|
if err != nil {
|
|
return step{}, errs.Wrap(err, errs.EditorCommand, "bad node id")
|
|
}
|
|
sp := s.Space(p)
|
|
switch op.Space {
|
|
case "2d":
|
|
sp = scene.Space2D
|
|
case "3d":
|
|
sp = scene.Space3D
|
|
}
|
|
if h, err = s.CreateIn(p, op.Name, id, sp); err != nil {
|
|
return step{}, err
|
|
}
|
|
case op.Space == "2d":
|
|
h = s.Create2D(p, op.Name)
|
|
case op.Space == "3d":
|
|
h = s.Create3D(p, op.Name)
|
|
case op.Space == "":
|
|
h = s.Create(p, op.Name)
|
|
default:
|
|
return step{}, errs.New(errs.EditorCommand, "space is 2d or 3d", log.F("space", op.Space))
|
|
}
|
|
s.Flush()
|
|
if err := d.place(h, p, op); err != nil {
|
|
s.Destroy(h)
|
|
s.Flush()
|
|
return step{}, err
|
|
}
|
|
res.Created = append(res.Created, idRef(s, h))
|
|
redo := Op{Kind: KindAddNode, Parent: idRef(s, p), Name: s.Name(h), ID: s.ID(h).String(), Space: spaceName(s.Space(h)),
|
|
Index: childIndex(s, h), HasIndex: true}
|
|
ch := d.change(op.Kind, h)
|
|
ch.After = idRef(s, h)
|
|
return step{inv: RemoveNode(idRef(s, h)), redo: redo, change: ch}, nil
|
|
}
|
|
|
|
func (d *Document) removeNode(op Op) (step, error) {
|
|
s := d.Scene
|
|
h, err := d.node(op.Node)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
if h == s.Root() {
|
|
return step{}, errs.New(errs.SceneBadReparent, "the root node cannot be removed")
|
|
}
|
|
ch := d.change(op.Kind, h)
|
|
inv := Op{Kind: KindInsert, Parent: idRef(s, s.Parent(h)), Index: childIndex(s, h), HasIndex: true,
|
|
Fragment: string(sceneio.SaveSubtree(s, h, false))}
|
|
redo := RemoveNode(idRef(s, h))
|
|
s.Destroy(h)
|
|
s.Flush()
|
|
return step{inv: inv, redo: redo, change: ch}, nil
|
|
}
|
|
|
|
func (d *Document) load(p scene.NodeHandle, fragment []byte, name string) (scene.NodeHandle, error) {
|
|
s := d.Scene
|
|
created, _, err := sceneio.LoadInto(s, p, fragment, name, sceneio.LoadOptions{KeepIDs: true, Prefabs: d.env.Library})
|
|
if err != nil {
|
|
return scene.NodeHandle{}, err
|
|
}
|
|
var root scene.NodeHandle
|
|
for _, h := range created {
|
|
if s.Parent(h) != p {
|
|
continue
|
|
}
|
|
if !root.IsZero() {
|
|
return root, errs.New(errs.EditorCommand, "a fragment must have one root node")
|
|
}
|
|
root = h
|
|
}
|
|
if root.IsZero() {
|
|
return root, errs.New(errs.SceneIOParse, "the fragment holds no node")
|
|
}
|
|
return root, nil
|
|
}
|
|
|
|
func (d *Document) insert(op Op, res *Result) (step, error) {
|
|
s := d.Scene
|
|
p, err := d.node(op.Parent)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
h, err := d.load(p, []byte(op.Fragment), "insert")
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
if err := d.place(h, p, op); err != nil {
|
|
return step{}, err
|
|
}
|
|
res.Created = append(res.Created, idRef(s, h))
|
|
redo := Op{Kind: KindInsert, Parent: idRef(s, p), Fragment: op.Fragment, Index: childIndex(s, h), HasIndex: true}
|
|
return step{inv: RemoveNode(idRef(s, h)), redo: redo, change: d.change(KindInsert, h)}, nil
|
|
}
|
|
|
|
func (d *Document) subtree(h scene.NodeHandle) []byte {
|
|
return sceneio.SaveSubtreeWith(d.Scene, h, d.library())
|
|
}
|
|
|
|
func (d *Document) replaceWith(h scene.NodeHandle, after []byte, kind string) (step, scene.NodeHandle, error) {
|
|
s := d.Scene
|
|
if h == s.Root() {
|
|
return step{}, h, errs.New(errs.SceneBadReparent, "the root node cannot be replaced")
|
|
}
|
|
parent, index, oldID := s.Parent(h), childIndex(s, h), idRef(s, h)
|
|
before := d.subtree(h)
|
|
s.Destroy(h)
|
|
s.Flush()
|
|
nh, err := d.load(parent, after, "replace")
|
|
if err == nil {
|
|
err = s.Reparent(nh, parent, index)
|
|
s.Flush()
|
|
}
|
|
if err != nil {
|
|
if back, berr := d.load(parent, before, "restore"); berr == nil {
|
|
s.Reparent(back, parent, index)
|
|
s.Flush()
|
|
}
|
|
return step{}, h, err
|
|
}
|
|
ch := d.change(kind, nh)
|
|
return step{inv: Replace(idRef(s, nh), string(before)), redo: Replace(oldID, string(after)), change: ch}, nh, nil
|
|
}
|
|
|
|
func (d *Document) move(op Op) (step, error) {
|
|
s := d.Scene
|
|
h, err := d.node(op.Node)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
np, err := d.node(op.Parent)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
before := d.nodeName(h)
|
|
old := s.Pose(h)
|
|
inv := Op{Kind: KindMove, Node: idRef(s, h), Parent: idRef(s, s.Parent(h)), Index: childIndex(s, h), HasIndex: true, Values: poseBlock(d.reg, old)}
|
|
pose, keep := old, false
|
|
switch {
|
|
case op.Values != nil:
|
|
if pose, err = d.readPose(old, op.Values); err != nil {
|
|
return step{}, err
|
|
}
|
|
keep = true
|
|
case op.KeepWorld:
|
|
if pose, err = PreserveWorld(s, h, np); err != nil {
|
|
return step{}, err
|
|
}
|
|
keep = true
|
|
}
|
|
index := -1
|
|
if op.HasIndex {
|
|
index = op.Index
|
|
}
|
|
if err := s.Reparent(h, np, index); err != nil {
|
|
return step{}, err
|
|
}
|
|
s.Flush()
|
|
if keep {
|
|
s.SetPose(h, pose)
|
|
}
|
|
redo := Op{Kind: KindMove, Node: idRef(s, h), Parent: idRef(s, np), Index: childIndex(s, h), HasIndex: true, Values: poseBlock(d.reg, s.Pose(h))}
|
|
ch := d.change(op.Kind, h)
|
|
ch.Node, ch.After = before, d.nodeName(h)
|
|
return step{inv: inv, redo: redo, change: ch}, nil
|
|
}
|
|
|
|
func (d *Document) rename(op Op) (step, error) {
|
|
s := d.Scene
|
|
h, err := d.node(op.Node)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
if h == s.Root() || op.Name == "" {
|
|
return step{}, errs.New(errs.EditorCommand, "rename needs a node below the root and a name")
|
|
}
|
|
before := s.Name(h)
|
|
after := s.SetName(h, op.Name)
|
|
ch := d.change(op.Kind, h)
|
|
ch.Before, ch.After = before, after
|
|
return step{inv: Rename(idRef(s, h), before), redo: Rename(idRef(s, h), after), change: ch}, nil
|
|
}
|
|
|
|
func (d *Document) values(td *schema.TypeDescriptor, p *aetext.Node, dst func(fd *schema.FieldDescriptor, v schema.Value) error) error {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
for _, f := range p.Fields {
|
|
fd, ok := field(td, f.Name)
|
|
if !ok {
|
|
return errs.New(errs.EditorProps, td.FullName+" has no field "+f.Name)
|
|
}
|
|
v, err := d.read(fd, f.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := dst(fd, v); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Document) snapshot(td *schema.TypeDescriptor, h scene.NodeHandle) *aetext.Node {
|
|
p, _ := d.Scene.GetByType(h, td.ID)
|
|
n := &aetext.Node{Head: "values"}
|
|
td.Serializable(func(fd *schema.FieldDescriptor) {
|
|
var v schema.Value
|
|
fd.Get(p, &v)
|
|
n.Fields = append(n.Fields, aetext.Field{Name: fd.Name, Value: sceneio.WriteValue(d.reg, fd, v)})
|
|
})
|
|
return n
|
|
}
|
|
|
|
func (d *Document) addComp(op Op, res *Result) (step, error) {
|
|
s := d.Scene
|
|
h, err := d.node(op.Node)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
td, err := d.attachable(op.Comp)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
if td.Kind == schema.TypeScript {
|
|
return d.addScript(op, h, td)
|
|
}
|
|
if _, ok := s.GetByType(h, td.ID); ok {
|
|
return step{}, errs.New(errs.EditorProps, "node already has "+td.FullName)
|
|
}
|
|
if !td.Space.Allows(s.Space(h).Kind()) {
|
|
return step{}, errs.New(errs.SceneSpaceMismatch, td.FullName+" does not fit the node space", log.F("space", s.Space(h).String()))
|
|
}
|
|
p, err := s.AddByType(h, td.ID)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
if !op.Raw && td.Default != nil && td.Copy != nil {
|
|
td.Copy(p, td.Default())
|
|
}
|
|
err = d.values(td, op.Values, func(fd *schema.FieldDescriptor, v schema.Value) error {
|
|
if !op.Raw {
|
|
if err := d.validate(fd, v, td.FullName+"."+fd.Name, op.Live); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return fd.Set(p, v)
|
|
})
|
|
if err != nil {
|
|
s.RemoveByType(h, td.ID)
|
|
s.Flush()
|
|
return step{}, err
|
|
}
|
|
if !op.Raw && d.env.OnAdd != nil {
|
|
d.env.OnAdd(td, s, h)
|
|
}
|
|
s.Flush()
|
|
for _, req := range td.Requires {
|
|
if rt, ok := d.reg.ByName(req); ok {
|
|
if _, has := s.GetByType(h, rt.ID); !has {
|
|
res.Problems = append(res.Problems, Problem{Node: d.nodeName(h), Message: td.FullName + " needs " + req + " on the same node"})
|
|
}
|
|
}
|
|
}
|
|
for _, alts := range td.RequiresAny {
|
|
if !hasAny(d.reg, s, h, alts) {
|
|
res.Problems = append(res.Problems, Problem{Node: d.nodeName(h), Message: td.FullName + " needs one of " + strings.Join(alts, ", ") + " on the same node"})
|
|
}
|
|
}
|
|
if other, ok := Conflict(s, td, h); ok {
|
|
res.Problems = append(res.Problems, Problem{Node: d.nodeName(h), Message: td.FullName + " and " + other.FullName + " cannot share a node"})
|
|
}
|
|
redo := Op{Kind: KindAddComp, Node: idRef(s, h), Comp: td.FullName, Raw: true, Values: d.snapshot(td, h)}
|
|
ch := d.change(op.Kind, h)
|
|
ch.Path, ch.Type = td.FullName, td.ID
|
|
return step{inv: RemoveComp(idRef(s, h), td.FullName), redo: redo, change: ch}, nil
|
|
}
|
|
|
|
func (d *Document) removeComp(op Op, res *Result) (step, error) {
|
|
s := d.Scene
|
|
h, err := d.node(op.Node)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
td, err := d.attachable(op.Comp)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
if td.Kind == schema.TypeScript {
|
|
return d.removeScript(op, h, td)
|
|
}
|
|
if _, ok := s.GetByType(h, td.ID); !ok {
|
|
return step{}, errs.New(errs.SceneNoComponent, "node has no "+td.FullName)
|
|
}
|
|
inv := Op{Kind: KindAddComp, Node: idRef(s, h), Comp: td.FullName, Raw: true, Values: d.snapshot(td, h)}
|
|
if td.ID == scriptcomp.ScriptTypeID {
|
|
inv = Replace(idRef(s, h), string(d.subtree(h)))
|
|
}
|
|
if d.env.OnRemove != nil {
|
|
d.env.OnRemove(td, s, h)
|
|
}
|
|
s.RemoveByType(h, td.ID)
|
|
s.Flush()
|
|
for _, other := range d.reg.Components() {
|
|
if _, has := s.GetByType(h, other.ID); has && slices.Contains(other.Requires, td.FullName) {
|
|
res.Problems = append(res.Problems, Problem{Node: d.nodeName(h), Message: other.FullName + " needs " + td.FullName + " on the same node"})
|
|
}
|
|
if _, has := s.GetByType(h, other.ID); has {
|
|
for _, alts := range other.RequiresAny {
|
|
if slices.Contains(alts, td.FullName) && !hasAny(d.reg, s, h, alts) {
|
|
res.Problems = append(res.Problems, Problem{Node: d.nodeName(h), Message: other.FullName + " needs one of " + strings.Join(alts, ", ") + " on the same node"})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ch := d.change(op.Kind, h)
|
|
ch.Path, ch.Type = td.FullName, td.ID
|
|
return step{inv: inv, redo: RemoveComp(idRef(s, h), td.FullName), change: ch}, nil
|
|
}
|
|
|
|
func hasAny(reg *schema.Registry, s *scene.Scene, h scene.NodeHandle, names []string) bool {
|
|
for _, n := range names {
|
|
if t, ok := reg.ByName(n); ok {
|
|
if _, has := s.GetByType(h, t.ID); has {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func defaultOf(td *schema.TypeDescriptor, fd *schema.FieldDescriptor) schema.Value {
|
|
var v schema.Value
|
|
switch {
|
|
case td.Default != nil:
|
|
fd.Get(td.Default(), &v)
|
|
case td.New != nil:
|
|
fd.Get(td.New(), &v)
|
|
case fd.HasDefault:
|
|
v = fd.Default
|
|
default:
|
|
v.Kind = fd.Kind
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (d *Document) next(fd *schema.FieldDescriptor, cur schema.Value, raw aetext.Value, lane string, index int, path string) (schema.Value, error) {
|
|
switch {
|
|
case lane != "":
|
|
return setLane(cur, lane, raw)
|
|
case index >= 0:
|
|
if fd.Kind != schema.KindList || index >= len(cur.List) {
|
|
return cur, errs.New(errs.EditorProps, "index out of range", log.F("path", path), log.F("len", len(cur.List)))
|
|
}
|
|
el, err := d.read(elemOf(fd), raw)
|
|
if err != nil {
|
|
return cur, err
|
|
}
|
|
next := cur
|
|
next.List = slices.Clone(cur.List)
|
|
next.List[index] = el
|
|
return next, nil
|
|
}
|
|
return d.read(fd, raw)
|
|
}
|
|
|
|
func (d *Document) set(op Op, raw aetext.Value, reset bool) (step, error) {
|
|
t, err := d.target(op.Node, op.Path)
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
if t.pose != "" {
|
|
return d.setPose(op, t, raw, reset)
|
|
}
|
|
s := d.Scene
|
|
cur, err := s.GetProperty(t.h, t.td.ID, t.fd.ID)
|
|
if err != nil {
|
|
return step{}, errs.New(errs.SceneNoComponent, "node has no "+t.td.FullName, log.F("node", d.nodeName(t.h)))
|
|
}
|
|
var nv schema.Value
|
|
if reset {
|
|
nv = defaultOf(t.td, t.fd)
|
|
} else if nv, err = d.next(t.fd, cur, raw, t.lane, t.index, op.Path); err != nil {
|
|
return step{}, err
|
|
}
|
|
if err := d.validate(t.fd, nv, t.base, op.Live); err != nil {
|
|
return step{}, err
|
|
}
|
|
if err := s.SetProperty(t.h, t.td.ID, t.fd.ID, nv); err != nil {
|
|
return step{}, err
|
|
}
|
|
inv := SetValue(idRef(s, t.h), t.base, sceneio.WriteValue(d.reg, t.fd, cur))
|
|
redo := SetValue(idRef(s, t.h), t.base, sceneio.WriteValue(d.reg, t.fd, nv))
|
|
inv.Live, redo.Live = op.Live, op.Live
|
|
ch := d.change(op.Kind, t.h)
|
|
ch.Path, ch.Before, ch.After = t.base, d.text(t.fd, cur), d.text(t.fd, nv)
|
|
ch.Type, ch.Field = t.td.ID, t.fd.ID
|
|
return step{inv: inv, redo: redo, change: ch}, nil
|
|
}
|
|
|
|
func (d *Document) poseValue(t target) (schema.Value, *schema.FieldDescriptor) {
|
|
s := d.Scene
|
|
if t.pose == pathEnabled {
|
|
return schema.Bool(s.Enabled(t.h)), plain(schema.KindBool)
|
|
}
|
|
pose := s.Pose(t.h)
|
|
if pose.Space == scene.Space3D {
|
|
switch t.pose {
|
|
case "position":
|
|
return schema.Vec3(pose.T3.Position), plain(schema.KindVec3)
|
|
case "rotation":
|
|
return schema.Quat(pose.T3.Rotation), plain(schema.KindQuat)
|
|
}
|
|
return schema.Vec3(pose.T3.Scale), plain(schema.KindVec3)
|
|
}
|
|
switch t.pose {
|
|
case "position":
|
|
return schema.Vec2(pose.T2.Position), plain(schema.KindVec2)
|
|
case "rotation":
|
|
return schema.Float32(pose.T2.Rotation), plain(schema.KindFloat32)
|
|
}
|
|
return schema.Vec2(pose.T2.Scale), plain(schema.KindVec2)
|
|
}
|
|
|
|
func (d *Document) writePose(t target, v schema.Value) {
|
|
s := d.Scene
|
|
if t.pose == pathEnabled {
|
|
s.SetEnabled(t.h, v.I != 0)
|
|
return
|
|
}
|
|
pose := s.Pose(t.h)
|
|
if pose.Space == scene.Space3D {
|
|
switch t.pose {
|
|
case "position":
|
|
pose.T3.Position = v.V3
|
|
case "rotation":
|
|
pose.T3.Rotation = v.Q
|
|
default:
|
|
pose.T3.Scale = v.V3
|
|
}
|
|
} else {
|
|
switch t.pose {
|
|
case "position":
|
|
pose.T2.Position = v.V
|
|
case "rotation":
|
|
pose.T2.Rotation = float32(v.F)
|
|
default:
|
|
pose.T2.Scale = v.V
|
|
}
|
|
}
|
|
s.SetPose(t.h, pose)
|
|
}
|
|
|
|
func identity(t target, cur schema.Value) schema.Value {
|
|
next := schema.Value{Kind: cur.Kind}
|
|
switch {
|
|
case t.pose == pathEnabled:
|
|
next.I = 1
|
|
case t.pose == "scale" && cur.Kind == schema.KindVec2:
|
|
next.V.X, next.V.Y = 1, 1
|
|
case t.pose == "scale":
|
|
next.V3.X, next.V3.Y, next.V3.Z = 1, 1, 1
|
|
case cur.Kind == schema.KindQuat:
|
|
next.Q.W = 1
|
|
}
|
|
return next
|
|
}
|
|
|
|
func (d *Document) setPose(op Op, t target, raw aetext.Value, reset bool) (step, error) {
|
|
s := d.Scene
|
|
if t.h == s.Root() {
|
|
return step{}, errs.New(errs.EditorProps, "the root node has no transform")
|
|
}
|
|
cur, fd := d.poseValue(t)
|
|
var nv schema.Value
|
|
var err error
|
|
switch {
|
|
case reset:
|
|
nv = identity(t, cur)
|
|
case t.lane != "":
|
|
nv, err = setLane(cur, t.lane, raw)
|
|
default:
|
|
nv, err = d.read(fd, raw)
|
|
}
|
|
if err != nil {
|
|
return step{}, err
|
|
}
|
|
if err := d.check(fd, nv, t.base); err != nil {
|
|
return step{}, err
|
|
}
|
|
d.writePose(t, nv)
|
|
ch := d.change(op.Kind, t.h)
|
|
ch.Path, ch.Before, ch.After = t.base, d.text(fd, cur), d.text(fd, nv)
|
|
return step{
|
|
inv: SetValue(idRef(s, t.h), t.base, sceneio.WriteValue(d.reg, fd, cur)),
|
|
redo: SetValue(idRef(s, t.h), t.base, sceneio.WriteValue(d.reg, fd, nv)),
|
|
change: ch,
|
|
}, nil
|
|
}
|
|
|
|
// addScript attaches a script to a node, with its exported fields at their defaults unless op sets them.
|
|
func (d *Document) addScript(op Op, h scene.NodeHandle, td *schema.TypeDescriptor) (step, error) {
|
|
s := d.Scene
|
|
if sc, ok := scriptcomp.ScriptsOf(s, h); ok && sc.Find(td) != nil {
|
|
return step{}, errs.New(errs.EditorProps, "node already has "+td.FullName)
|
|
}
|
|
inv := Replace(idRef(s, h), string(d.subtree(h)))
|
|
sc, ok := scriptcomp.ScriptsOf(s, h)
|
|
if !ok {
|
|
sc = scene.Add[scriptcomp.ScriptComponent](s, h)
|
|
}
|
|
sc.Attachments = append(sc.Attachments, scriptcomp.Attachment{TypeName: td.FullName, Enabled: true})
|
|
s.Flush()
|
|
p, ok := scriptcomp.Overlay{}.Bag(s, h, td)
|
|
if !ok {
|
|
return step{}, errs.New(errs.EditorProps, "cannot attach "+td.FullName)
|
|
}
|
|
err := d.values(td, op.Values, func(fd *schema.FieldDescriptor, v schema.Value) error {
|
|
if !op.Raw {
|
|
if err := d.validate(fd, v, td.FullName+"."+fd.Name, op.Live); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return fd.Set(p, v)
|
|
})
|
|
if err != nil {
|
|
if sc, ok := scriptcomp.ScriptsOf(s, h); ok {
|
|
if i := sc.Index(td.FullName); i >= 0 {
|
|
sc.Attachments = slices.Delete(sc.Attachments, i, i+1)
|
|
}
|
|
if len(sc.Attachments) == 0 {
|
|
scene.Remove[scriptcomp.ScriptComponent](s, h)
|
|
}
|
|
}
|
|
s.Flush()
|
|
return step{}, err
|
|
}
|
|
s.Flush()
|
|
redo := Replace(idRef(s, h), string(d.subtree(h)))
|
|
ch := d.change(op.Kind, h)
|
|
ch.Path, ch.Type = td.FullName, td.ID
|
|
return step{inv: inv, redo: redo, change: ch}, nil
|
|
}
|
|
|
|
// removeScript detaches a script; undo puts the node back as it was, with the script's field values.
|
|
func (d *Document) removeScript(op Op, h scene.NodeHandle, td *schema.TypeDescriptor) (step, error) {
|
|
s := d.Scene
|
|
inv := Replace(idRef(s, h), string(d.subtree(h)))
|
|
sc, ok := scriptcomp.ScriptsOf(s, h)
|
|
i := -1
|
|
if ok {
|
|
i = sc.Index(td.FullName)
|
|
}
|
|
if i < 0 {
|
|
return step{}, errs.New(errs.SceneNoComponent, "node has no "+td.FullName)
|
|
}
|
|
sc.Attachments = slices.Delete(sc.Attachments, i, i+1)
|
|
if len(sc.Attachments) == 0 {
|
|
scene.Remove[scriptcomp.ScriptComponent](s, h)
|
|
}
|
|
s.Flush()
|
|
ch := d.change(op.Kind, h)
|
|
ch.Path, ch.Type = td.FullName, td.ID
|
|
return step{inv: inv, redo: Replace(idRef(s, h), string(d.subtree(h))), change: ch}, nil
|
|
}
|