ci / release (push) Successful in 5m5s
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 / checks (push) Successful in 4m41s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m14s
The remaining editor strings and undo step names; component, field, enum, setting, diagnostic and Script API texts under schema.*, enum.*, setting.*, diag.* and api.*; the launcher, the in-game Settings View with a Language setting, the CLI and aego schema show --lang. Every documentation page has a .ru.md translation; the site has a full Russian tree with hreflang, a language switch and ru/llms.txt; aego docs check --lang ru. New doc.tutorial and doc.recipe icons. Fix: an animator restored by a rewind prepares its controller before the replayed step.
381 lines
12 KiB
Go
381 lines
12 KiB
Go
package animator
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"aego/anim"
|
|
"aego/core/mathx"
|
|
"aego/editor/core/document"
|
|
"aego/engine/i18n"
|
|
"aego/format/aetext"
|
|
"aego/scene/mutate"
|
|
"aego/ui/core"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
var (
|
|
kindNames = func() []string {
|
|
return []string{"Bool", "Int", "Float", i18n.Tr("editor.animator.kind.trigger", "Trigger")}
|
|
}
|
|
opNames = []string{"set", "==", "!=", ">", ">=", "<", "<="}
|
|
)
|
|
|
|
// SetEntry makes a state the one the Animator starts in.
|
|
func (p *Panel) SetEntry(i int) error {
|
|
return p.Edit(i18n.Tr("editor.history.set_entry_state", "Set Entry State"), mutate.CtrlRoot("entry", aetext.String(p.g.States[i].Name)))
|
|
}
|
|
|
|
// MoveTransition moves a transition up or down; earlier transitions are tried first.
|
|
func (p *Panel) MoveTransition(i, delta int) error {
|
|
err := p.Edit(i18n.Tr("editor.history.move_transition", "Move Transition"), mutate.CtrlMove(i, delta))
|
|
if err == nil && p.sel.kind == selTransition && p.sel.index == i {
|
|
p.sel.index = i + delta
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (p *Panel) RemoveTransition(i int) error {
|
|
p.sel = selection{}
|
|
return p.Edit(i18n.Tr("editor.history.delete_transition", "Delete Transition"), mutate.CtrlRemove("transition", "").At(i))
|
|
}
|
|
|
|
// AddParam adds a parameter of a kind with a free name.
|
|
func (p *Panel) AddParam(kind anim.ParamKind) error {
|
|
name := kind.String()
|
|
for n := 1; p.g.ParamIndex(name) >= 0; n++ {
|
|
name = kind.String() + strconv.Itoa(n+1)
|
|
}
|
|
body := &aetext.Node{Attrs: []aetext.Field{{Name: "kind", Value: aetext.Ident(kind.String())}}}
|
|
return p.Edit(i18n.Tr("editor.history.add_parameter", "Add Parameter"), mutate.CtrlPut("param", name, body))
|
|
}
|
|
|
|
// transitionBody is the text block of transition i, to edit its conditions.
|
|
func (p *Panel) transitionBody(i int) *aetext.Node {
|
|
n := 0
|
|
for _, b := range p.doc.Root().Blocks {
|
|
if b.Head != "transition" {
|
|
continue
|
|
}
|
|
if n == i {
|
|
return b.Clone()
|
|
}
|
|
n++
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Panel) putTransition(i int, label string, body *aetext.Node) error {
|
|
return p.Edit(label, mutate.CtrlPut("transition", "", body).At(i))
|
|
}
|
|
|
|
func condValue(kind anim.ParamKind, v float64) aetext.Value {
|
|
switch kind {
|
|
case anim.ParamBool:
|
|
return aetext.Bool(v != 0)
|
|
case anim.ParamInt:
|
|
return aetext.Int(int64(v))
|
|
}
|
|
return aetext.Float64(v)
|
|
}
|
|
|
|
// SetCond replaces condition c of transition i; c equal to the count adds one.
|
|
func (p *Panel) SetCond(i, c int, cond anim.Cond) error {
|
|
body := p.transitionBody(i)
|
|
if body == nil || cond.Param < 0 || int(cond.Param) >= len(p.g.Params) {
|
|
return nil
|
|
}
|
|
pr := p.g.Params[cond.Param]
|
|
n := &aetext.Node{Head: "cond", Label: pr.Name}
|
|
if pr.Kind != anim.ParamTrigger {
|
|
op := cond.Op
|
|
if op == anim.OpSet {
|
|
op = defaultOp(pr.Kind)
|
|
}
|
|
n.Attrs = append(n.Attrs,
|
|
aetext.Field{Name: "op", Value: aetext.Ident(op.String())},
|
|
aetext.Field{Name: "value", Value: condValue(pr.Kind, cond.Value)})
|
|
}
|
|
k := 0
|
|
for j, b := range body.Blocks {
|
|
if b.Head != "cond" {
|
|
continue
|
|
}
|
|
if k == c {
|
|
body.Blocks[j] = n
|
|
return p.putTransition(i, i18n.Tr("editor.history.edit_condition", "Edit Condition"), body)
|
|
}
|
|
k++
|
|
}
|
|
body.Blocks = append(body.Blocks, n)
|
|
return p.putTransition(i, i18n.Tr("editor.history.add_condition", "Add Condition"), body)
|
|
}
|
|
|
|
func (p *Panel) RemoveCond(i, c int) error {
|
|
body := p.transitionBody(i)
|
|
if body == nil {
|
|
return nil
|
|
}
|
|
k := 0
|
|
for j, b := range body.Blocks {
|
|
if b.Head != "cond" {
|
|
continue
|
|
}
|
|
if k == c {
|
|
body.Blocks = append(body.Blocks[:j], body.Blocks[j+1:]...)
|
|
return p.putTransition(i, i18n.Tr("editor.history.remove_condition", "Remove Condition"), body)
|
|
}
|
|
k++
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Panel) sidebar(d *document.SceneDocument, r mathx.Rect) {
|
|
ui := p.f.UI
|
|
th := &ui.Theme
|
|
ui.Rect(r, th.Panel)
|
|
if ui.MouseOver(ui.ID("side"), r) {
|
|
p.scroll -= ui.ScrollY(th.RowHeight * 3)
|
|
}
|
|
p.scroll = mathx.Clamp(p.scroll, 0, max(0, p.content-r.H))
|
|
top := r.Y + th.Padding - p.scroll
|
|
ui.PushCursor(core.Cursor{X: r.X + th.Padding, Y: top, W: r.W - 2*th.Padding, RowH: th.RowHeight, Pad: th.Padding, Space: 3})
|
|
ui.PushClip(r)
|
|
switch p.sel.kind {
|
|
case selState:
|
|
p.stateSection(p.sel.index)
|
|
case selTransition:
|
|
p.transitionSection(p.sel.index)
|
|
default:
|
|
widgets.LabelDim(ui, i18n.Tr("editor.animator.select_a_state_or_a", "Select a state or a transition"))
|
|
widgets.LabelWrapped(ui, i18n.Tr("editor.animator.drag_from_a_state_s", "Drag from a state's dot to another state to add a transition"), true)
|
|
}
|
|
ui.Cursor().Next(6)
|
|
p.paramsSection()
|
|
p.content = ui.Cursor().Y - top + th.Padding
|
|
ui.PopClip()
|
|
ui.PopCursor()
|
|
}
|
|
|
|
func (p *Panel) header(text string) {
|
|
ui := p.f.UI
|
|
r := ui.Cursor().Next(0)
|
|
ui.Text(text, mathx.Vec2{X: r.X, Y: ui.TextY(r)}, ui.Theme.TextDim)
|
|
}
|
|
|
|
func (p *Panel) paramsSection() {
|
|
ui := p.f.UI
|
|
th := &ui.Theme
|
|
head := ui.Cursor().Next(0)
|
|
ui.Text(i18n.Tr("editor.animator.parameters", "Parameters"), mathx.Vec2{X: head.X, Y: ui.TextY(head)}, th.TextDim)
|
|
add := mathx.Rect{X: head.X + head.W - head.H, Y: head.Y, W: head.H, H: head.H}
|
|
aid := ui.ID("addparam")
|
|
if widgets.IconButton(ui, aid, add, "ui.plus", i18n.Tr("editor.animator.add_a_parameter", "Add a parameter")) {
|
|
ui.CloseAllPopups()
|
|
ui.OpenPopup(aid, mathx.Vec2{X: add.X, Y: add.Y + add.H})
|
|
}
|
|
if ui.BeginPopup(aid, 140) {
|
|
for k, n := range kindNames() {
|
|
if widgets.MenuItem(ui, n, "", true, false) {
|
|
p.notify(p.AddParam(anim.ParamKind(k)))
|
|
}
|
|
}
|
|
ui.EndPopup()
|
|
}
|
|
var live Live
|
|
if p.d.Live != nil {
|
|
live = p.d.Live()
|
|
}
|
|
for i, pr := range p.g.Params {
|
|
row := ui.Cursor().Next(0)
|
|
lw := row.W * 0.55
|
|
ui.PushClip(mathx.Rect{X: row.X, Y: row.Y, W: lw - 4, H: row.H})
|
|
ui.Text(p.params[i], mathx.Vec2{X: row.X, Y: ui.TextY(row)}, th.Text)
|
|
ui.PopClip()
|
|
vr := mathx.Rect{X: row.X + lw, Y: row.Y, W: row.W - lw, H: row.H}
|
|
id := core.IDSeq(ui.ID("param"), i)
|
|
if live == nil {
|
|
if widgets.IconButton(ui, id, mathx.Rect{X: vr.X + vr.W - vr.H, Y: vr.Y, W: vr.H, H: vr.H}, "ui.minus", i18n.Tr("editor.animator.remove_the_parameter", "Remove the parameter")) {
|
|
p.notify(p.Edit(i18n.Tr("editor.history.remove_parameter", "Remove Parameter"), mutate.CtrlRemove("param", pr.Name)))
|
|
}
|
|
continue
|
|
}
|
|
v, _ := live.Param(p.node, pr.Name)
|
|
switch pr.Kind {
|
|
case anim.ParamTrigger:
|
|
if widgets.ButtonRect(ui, id, vr, i18n.Tr("editor.animator.fire", "Fire")) {
|
|
live.SetParam(p.node, pr.Name, 1)
|
|
}
|
|
case anim.ParamBool:
|
|
on := v != 0
|
|
box := mathx.Rect{X: vr.X, Y: vr.Y + 3, W: vr.H - 6, H: vr.H - 6}
|
|
if ui.Clicked(id, box) {
|
|
live.SetParam(p.node, pr.Name, flag(!on))
|
|
}
|
|
if on {
|
|
ui.RoundRect(box, th.Radius.Field, th.Accent)
|
|
widgets.CheckMark(ui, box, th.Background)
|
|
} else {
|
|
ui.Box(box, th.Radius.Field, th.Widget, th.Border)
|
|
}
|
|
default:
|
|
f := float32(v)
|
|
if e := widgets.DragFloat32(ui, id, vr, &f, widgets.NumberOpts{Speed: 0.05}); e.Changed {
|
|
live.SetParam(p.node, pr.Name, float64(f))
|
|
}
|
|
}
|
|
}
|
|
if len(p.g.Params) == 0 {
|
|
widgets.LabelDim(ui, i18n.Tr("editor.animator.no_parameters", "No parameters"))
|
|
}
|
|
}
|
|
|
|
func flag(b bool) float64 {
|
|
if b {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (p *Panel) stateSection(i int) {
|
|
ui := p.f.UI
|
|
s := &p.g.States[i]
|
|
p.header(p.stateHeads[i])
|
|
if p.d.Clips != nil {
|
|
if p.clips == nil {
|
|
p.clips = p.d.Clips()
|
|
p.clipNames = p.clipNames[:0]
|
|
for _, c := range p.clips {
|
|
p.clipNames = append(p.clipNames, c.Name)
|
|
}
|
|
}
|
|
cur := -1
|
|
for k, c := range p.clips {
|
|
if c.ID == s.Clip {
|
|
cur = k
|
|
}
|
|
}
|
|
p.clipID = ui.ID("clip")
|
|
p.clipRect = widgets.Field(ui, i18n.Tr("editor.animator.clip", "Clip"))
|
|
if widgets.Combo(ui, p.clipID, p.clipRect, &cur, p.clipNames, cur < 0) && cur >= 0 {
|
|
p.notify(p.Edit(i18n.Tr("editor.history.set_clip", "Set Clip"), mutate.CtrlAttr("state", s.Name, "clip", aetext.AssetRef(p.clips[cur].ID.String()))))
|
|
}
|
|
}
|
|
speed := s.Speed
|
|
if e := widgets.DragFloat32(ui, ui.ID("speed"), widgets.Field(ui, i18n.Tr("editor.animator.speed", "Speed")), &speed, widgets.NumberOpts{Speed: 0.01}); e.End && speed != s.Speed {
|
|
p.notify(p.Edit(i18n.Tr("editor.history.set_speed", "Set Speed"), mutate.CtrlAttr("state", s.Name, "speed", aetext.Float32(speed))))
|
|
}
|
|
if int32(i) != p.g.Entry && widgets.Button(ui, i18n.Tr("editor.animator.set_as_entry", "Set as Entry")) {
|
|
p.notify(p.SetEntry(i))
|
|
}
|
|
if widgets.Button(ui, i18n.Tr("editor.animator.delete_state", "Delete State")) {
|
|
p.notify(p.RemoveState(i))
|
|
}
|
|
}
|
|
|
|
func (p *Panel) transitionSection(i int) {
|
|
ui := p.f.UI
|
|
th := &ui.Theme
|
|
g := p.g
|
|
t := &g.Transitions[i]
|
|
row := ui.Cursor().Next(0)
|
|
ui.Text(p.heads[i], mathx.Vec2{X: row.X, Y: ui.TextY(row)}, th.TextDim)
|
|
up := mathx.Rect{X: row.X + row.W - 2*row.H - 2, Y: row.Y, W: row.H, H: row.H}
|
|
down := mathx.Rect{X: row.X + row.W - row.H, Y: row.Y, W: row.H, H: row.H}
|
|
if i > 0 && widgets.IconButton(ui, ui.ID("up"), up, "ui.move_up", i18n.Tr("editor.animator.try_earlier", "Try earlier")) {
|
|
p.notify(p.MoveTransition(i, -1))
|
|
return
|
|
}
|
|
if i+1 < len(g.Transitions) && widgets.IconButton(ui, ui.ID("down"), down, "ui.move_down", i18n.Tr("editor.animator.try_later", "Try later")) {
|
|
p.notify(p.MoveTransition(i, 1))
|
|
return
|
|
}
|
|
attr := func(name string, v aetext.Value) {
|
|
p.notify(p.Edit(i18n.Tr("editor.history.edit_transition", "Edit Transition"), mutate.CtrlAttr("transition", "", name, v).At(i)))
|
|
}
|
|
dur := t.Duration
|
|
if e := widgets.DragFloat32(ui, ui.ID("dur"), widgets.Field(ui, i18n.Tr("editor.animator.duration", "Duration")), &dur, widgets.NumberOpts{Speed: 0.01, Min: 0, HasMin: true}); e.End && dur != t.Duration {
|
|
attr("duration", aetext.Float32(dur))
|
|
}
|
|
exit := t.HasExit
|
|
if widgets.Checkbox(ui, i18n.Tr("editor.animator.exit_time", "Exit Time"), &exit) {
|
|
if exit {
|
|
attr("exitTime", aetext.Float32(1))
|
|
} else {
|
|
p.notify(p.Edit(i18n.Tr("editor.history.edit_transition", "Edit Transition"), mutate.CtrlAttrClear("transition", "", "exitTime").At(i)))
|
|
}
|
|
}
|
|
if t.HasExit {
|
|
et := t.ExitTime
|
|
if e := widgets.DragFloat32(ui, ui.ID("exit"), widgets.Field(ui, i18n.Tr("editor.animator.at", "At")), &et, widgets.NumberOpts{Speed: 0.01, Min: 0, HasMin: true}); e.End && et != t.ExitTime {
|
|
attr("exitTime", aetext.Float32(et))
|
|
}
|
|
}
|
|
intr := t.Interrupt
|
|
if widgets.Checkbox(ui, i18n.Tr("editor.animator.interrupts", "Interrupts"), &intr) {
|
|
attr("interrupt", aetext.Bool(intr))
|
|
}
|
|
p.header(i18n.Tr("editor.animator.conditions", "Conditions"))
|
|
for c, cond := range t.Conds {
|
|
p.condRow(i, c, cond, p.names)
|
|
}
|
|
if len(g.Params) > 0 && widgets.Button(ui, i18n.Tr("editor.animator.add_condition", "Add Condition")) {
|
|
p.notify(p.SetCond(i, len(t.Conds), anim.Cond{Param: 0, Op: defaultOp(g.Params[0].Kind)}))
|
|
}
|
|
if widgets.Button(ui, i18n.Tr("editor.animator.delete_transition", "Delete Transition")) {
|
|
p.notify(p.RemoveTransition(i))
|
|
}
|
|
}
|
|
|
|
func defaultOp(k anim.ParamKind) anim.Op {
|
|
switch k {
|
|
case anim.ParamTrigger:
|
|
return anim.OpSet
|
|
case anim.ParamBool:
|
|
return anim.OpEq
|
|
}
|
|
return anim.OpGt
|
|
}
|
|
|
|
func (p *Panel) condRow(i, c int, cond anim.Cond, names []string) {
|
|
ui := p.f.UI
|
|
row := ui.Cursor().Next(0)
|
|
ui.PushIDInt(c)
|
|
defer ui.PopID()
|
|
w := (row.W - row.H - 8) / 3
|
|
pi := int(cond.Param)
|
|
if widgets.Combo(ui, ui.ID("param"), mathx.Rect{X: row.X, Y: row.Y, W: w, H: row.H}, &pi, names, false) {
|
|
p.notify(p.SetCond(i, c, anim.Cond{Param: int32(pi), Op: defaultOp(p.g.Params[pi].Kind), Value: cond.Value}))
|
|
return
|
|
}
|
|
kind := anim.ParamTrigger
|
|
if cond.Param >= 0 && int(cond.Param) < len(p.g.Params) {
|
|
kind = p.g.Params[cond.Param].Kind
|
|
}
|
|
switch kind {
|
|
case anim.ParamFloat, anim.ParamInt:
|
|
op := int(cond.Op)
|
|
if widgets.Combo(ui, ui.ID("op"), mathx.Rect{X: row.X + w + 4, Y: row.Y, W: w * 0.6, H: row.H}, &op, opNames, false) && op > 0 {
|
|
p.notify(p.SetCond(i, c, anim.Cond{Param: cond.Param, Op: anim.Op(op), Value: cond.Value}))
|
|
return
|
|
}
|
|
v := float32(cond.Value)
|
|
if e := widgets.DragFloat32(ui, ui.ID("v"), mathx.Rect{X: row.X + w*1.6 + 8, Y: row.Y, W: w * 1.4, H: row.H}, &v, widgets.NumberOpts{Speed: 0.05}); e.End && float64(v) != cond.Value {
|
|
p.notify(p.SetCond(i, c, anim.Cond{Param: cond.Param, Op: cond.Op, Value: float64(v)}))
|
|
return
|
|
}
|
|
case anim.ParamBool:
|
|
on := cond.Value != 0
|
|
label := "is false"
|
|
if on {
|
|
label = "is true"
|
|
}
|
|
if widgets.ButtonRect(ui, ui.ID("b"), mathx.Rect{X: row.X + w + 4, Y: row.Y, W: w * 2, H: row.H}, label) {
|
|
p.notify(p.SetCond(i, c, anim.Cond{Param: cond.Param, Op: anim.OpEq, Value: flag(!on)}))
|
|
return
|
|
}
|
|
}
|
|
if widgets.IconButton(ui, ui.ID("rm"), mathx.Rect{X: row.X + row.W - row.H, Y: row.Y, W: row.H, H: row.H}, "ui.minus", i18n.Tr("editor.animator.remove_the_condition", "Remove the condition")) {
|
|
p.notify(p.RemoveCond(i, c))
|
|
}
|
|
}
|