Files
aego/scene/mutate/apply.go
T

661 lines
17 KiB
Go

package mutate
import (
"errors"
"slices"
"strconv"
"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.component(op.Comp)
if err != nil {
return step{}, err
}
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"})
}
}
}
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.component(op.Comp)
if err != nil {
return step{}, err
}
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"})
}
}
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 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
p, ok := s.GetByType(t.h, t.td.ID)
if !ok {
return step{}, errs.New(errs.SceneNoComponent, "node has no "+t.td.FullName, log.F("node", d.nodeName(t.h)))
}
var cur schema.Value
t.fd.Get(p, &cur)
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
}