294 lines
7.7 KiB
Go
294 lines
7.7 KiB
Go
package editor
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"aego/core/mathx"
|
|
"aego/editor/core/context"
|
|
"aego/modules/physics/settings"
|
|
"aego/modules/physics/world"
|
|
"aego/ui/core"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
func intRow(ui *core.Context, label string, v *int, def int) bool {
|
|
f := float64(*v)
|
|
o := widgets.NumberOpts{Int: true, Speed: 0.1, Min: 0, Max: 64, HasMin: true, HasMax: true}
|
|
if *v == 0 {
|
|
f = float64(def)
|
|
o.Suffix = " default"
|
|
}
|
|
e := widgets.DragNumber(ui, ui.ID(label), widgets.Field(ui, label), &f, o)
|
|
if e.Changed {
|
|
*v = int(f)
|
|
}
|
|
return e.Changed
|
|
}
|
|
|
|
func floatRow(ui *core.Context, label string, v *float32, def float32) bool {
|
|
f := float64(*v)
|
|
o := widgets.NumberOpts{Speed: 0.01, Min: 0, HasMin: true, Prec: 4}
|
|
if *v == 0 {
|
|
f = float64(def)
|
|
o.Suffix = " default"
|
|
}
|
|
e := widgets.DragNumber(ui, ui.ID(label), widgets.Field(ui, label), &f, o)
|
|
if e.Changed {
|
|
*v = float32(f)
|
|
}
|
|
return e.Changed
|
|
}
|
|
|
|
func (p *plugin) settingsPage(ctx *context.Editor, draft any) bool {
|
|
ps, ok := draft.(*settings.Project)
|
|
if !ok {
|
|
return false
|
|
}
|
|
ui := ctx.UI
|
|
def := world.DefaultConfig()
|
|
changed := false
|
|
cfg := ps.Config()
|
|
p.preview(ui, cfg)
|
|
focus := ""
|
|
hot := func(label, part string) {
|
|
if ui.Hot() == ui.ID(label) || ui.Active() == ui.ID(label) {
|
|
focus = part
|
|
}
|
|
}
|
|
widgets.Label(ui, "Simulation")
|
|
g := ps.Gravity
|
|
if !ps.HasGravity {
|
|
g = def.Gravity
|
|
}
|
|
gid := ui.ID("gravity")
|
|
if e := widgets.Vec2Field(ui, gid, widgets.Field(ui, "Gravity"), &g, widgets.NumberOpts{Speed: 0.05}, false, false); e.Changed {
|
|
ps.Gravity, ps.HasGravity = g, true
|
|
changed = true
|
|
}
|
|
if h := ui.Hot(); h == core.IDSeq(gid, 0) || h == core.IDSeq(gid, 1) || ui.Active() == core.IDSeq(gid, 0) || ui.Active() == core.IDSeq(gid, 1) {
|
|
focus = "gravity"
|
|
}
|
|
for _, x := range []struct {
|
|
label string
|
|
v *int
|
|
def int
|
|
}{
|
|
{"Substeps", &ps.Substeps, def.Substeps},
|
|
{"Velocity Iterations", &ps.VelocityIterations, def.VelocityIterations},
|
|
{"Relax Iterations", &ps.RelaxIterations, def.RelaxIterations},
|
|
} {
|
|
changed = intRow(ui, x.label, x.v, x.def) || changed
|
|
hot(x.label, "solver")
|
|
}
|
|
for _, x := range []struct {
|
|
label string
|
|
part string
|
|
v *float32
|
|
def float32
|
|
}{
|
|
{"Contact Hertz", "contact", &ps.ContactHertz, def.ContactHertz},
|
|
{"Contact Damping Ratio", "contact", &ps.ContactDampingRatio, def.ContactDampingRatio},
|
|
{"Contact Push Velocity", "contact", &ps.ContactPushVelocity, def.ContactPushVelocity},
|
|
{"Restitution Threshold", "bounce", &ps.RestitutionThreshold, def.RestitutionThreshold},
|
|
{"Linear Slop", "contact", &ps.LinearSlop, def.LinearSlop},
|
|
{"Sleep Linear", "sleep", &ps.SleepLinear, def.SleepLinear},
|
|
{"Sleep Angular", "sleep", &ps.SleepAngular, def.SleepAngular},
|
|
{"Sleep Time", "sleep", &ps.SleepTime, def.SleepTime},
|
|
} {
|
|
changed = floatRow(ui, x.label, x.v, x.def) || changed
|
|
hot(x.label, x.part)
|
|
}
|
|
p.focus = focus
|
|
if err := ps.Validate(); err != nil {
|
|
widgets.LabelWrapped(ui, err.Error(), false)
|
|
}
|
|
widgets.LabelWrapped(ui, "Tall stacks and fast motors: Substeps 8 and Contact Hertz 60 (stiff profile) at about twice the solver cost. Zero keeps the engine default.", true)
|
|
widgets.Separator(ui)
|
|
widgets.Label(ui, "Layers")
|
|
changed = layers(ctx, ps) || changed
|
|
widgets.Separator(ui)
|
|
widgets.Label(ui, "Collision Matrix")
|
|
widgets.LabelDim(ui, "A checked cell means bodies on those two layers collide.")
|
|
changed = matrix(ui, ps) || changed
|
|
return changed
|
|
}
|
|
|
|
func ensureLayers(ps *settings.Project, n int) {
|
|
if len(ps.LayerNames) == 0 {
|
|
ps.LayerNames = []string{settings.DefaultLayer}
|
|
}
|
|
for len(ps.LayerNames) < n {
|
|
ps.LayerNames = append(ps.LayerNames, "")
|
|
}
|
|
}
|
|
|
|
func renameRules(ps *settings.Project, old, next string) {
|
|
out := ps.Collide[:0]
|
|
for _, r := range ps.Collide {
|
|
if r.Layer == old {
|
|
if next == "" {
|
|
continue
|
|
}
|
|
r.Layer = next
|
|
}
|
|
with := r.With[:0]
|
|
for _, w := range r.With {
|
|
switch {
|
|
case w != old:
|
|
with = append(with, w)
|
|
case next != "":
|
|
with = append(with, next)
|
|
}
|
|
}
|
|
r.With = with
|
|
out = append(out, r)
|
|
}
|
|
ps.Collide = out
|
|
}
|
|
|
|
func layers(ctx *context.Editor, ps *settings.Project) bool {
|
|
ui := ctx.UI
|
|
th := &ui.Theme
|
|
names := ps.Layers()
|
|
changed := false
|
|
last := 0
|
|
for i, n := range names {
|
|
if n != "" {
|
|
last = i
|
|
}
|
|
}
|
|
for i := 0; i <= last; i++ {
|
|
ui.PushIDInt(i)
|
|
row := ui.Cursor().Next(0)
|
|
chip := mathx.Rect{X: row.X, Y: row.Y + 2, W: 24, H: row.H - 4}
|
|
ui.RoundRect(chip, th.Radius.Button, th.Badge)
|
|
ui.TextIn(chip, strconv.Itoa(i), th.TextDim, core.AlignCenter)
|
|
fr := mathx.Rect{X: row.X + 30, Y: row.Y, W: row.W - 30 - row.H - 4, H: row.H}
|
|
name := names[i]
|
|
if _, ok := widgets.TextFieldRect(ui, ui.ID("name"), fr, &name, false); ok && name != names[i] {
|
|
switch {
|
|
case name != "" && !settings.ValidLayerName(name):
|
|
ctx.Notify(context.LevelWarn, name+" is not a valid layer name")
|
|
case name != "" && dup(names, name):
|
|
ctx.Notify(context.LevelWarn, "layer "+name+" already exists")
|
|
default:
|
|
ensureLayers(ps, i+1)
|
|
ps.LayerNames[i] = name
|
|
renameRules(ps, names[i], name)
|
|
changed = true
|
|
}
|
|
}
|
|
if i > 0 && widgets.IconButton(ui, ui.ID("clear"), mathx.Rect{X: row.X + row.W - row.H, Y: row.Y, W: row.H, H: row.H}, "ui.minus", "Clear the name; bodies keep index "+strconv.Itoa(i)) {
|
|
ensureLayers(ps, i+1)
|
|
ps.LayerNames[i] = ""
|
|
renameRules(ps, names[i], "")
|
|
changed = true
|
|
}
|
|
ui.PopID()
|
|
}
|
|
if last < settings.MaxLayers-1 && widgets.Button(ui, "Add Layer") {
|
|
for i := 1; i < settings.MaxLayers; i++ {
|
|
if names[i] != "" {
|
|
continue
|
|
}
|
|
ensureLayers(ps, i+1)
|
|
ps.LayerNames[i] = "Layer" + strconv.Itoa(i)
|
|
changed = true
|
|
break
|
|
}
|
|
}
|
|
widgets.LabelDim(ui, "Bodies store the layer index; clearing a name keeps the index.")
|
|
return changed
|
|
}
|
|
|
|
func dup(names [settings.MaxLayers]string, n string) bool {
|
|
for _, x := range names {
|
|
if x == n {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matrix(ui *core.Context, ps *settings.Project) bool {
|
|
th := &ui.Theme
|
|
names := ps.Layers()
|
|
var idx []int
|
|
for i, n := range names {
|
|
if n != "" {
|
|
idx = append(idx, i)
|
|
}
|
|
}
|
|
m := ps.Matrix()
|
|
changed := false
|
|
cs := ui.Theme.RowHeight
|
|
labelW := float32(30)
|
|
for _, i := range idx {
|
|
labelW = max(labelW, ui.TextSize(strconv.Itoa(i)+" "+names[i]).X+16)
|
|
}
|
|
labelW = min(labelW, 160)
|
|
head := ui.Cursor().Next(0)
|
|
hotRow, hotCol := -1, -1
|
|
for row, i := range idx {
|
|
for col := 0; col <= row; col++ {
|
|
j := idx[col]
|
|
if ui.Hot() == ui.IDInt(i*64+j) {
|
|
hotRow, hotCol = row, col
|
|
}
|
|
}
|
|
}
|
|
for col, j := range idx {
|
|
cr := mathx.Rect{X: head.X + labelW + float32(col)*(cs+2), Y: head.Y, W: cs, H: head.H}
|
|
c := th.TextDim
|
|
if col == hotCol || col == hotRow {
|
|
c = th.Text
|
|
}
|
|
ui.TextIn(cr, strconv.Itoa(j), c, core.AlignCenter)
|
|
}
|
|
for row, i := range idx {
|
|
r := ui.Cursor().Next(0)
|
|
c := th.TextDim
|
|
if row == hotRow || row == hotCol {
|
|
c = th.Text
|
|
}
|
|
ui.Text(widgets.Elide(ui, strconv.Itoa(i)+" "+names[i], labelW-8), mathx.Vec2{X: r.X, Y: ui.TextY(r)}, c)
|
|
for col := 0; col <= row; col++ {
|
|
j := idx[col]
|
|
cell := mathx.Rect{X: r.X + labelW + float32(col)*(cs+2), Y: r.Y, W: cs, H: r.H}
|
|
id := ui.IDInt(i*64 + j)
|
|
on := (m[i]>>j)&1 == 1
|
|
if ui.Clicked(id, cell) {
|
|
on = !on
|
|
if on {
|
|
m[i] |= 1 << j
|
|
m[j] |= 1 << i
|
|
} else {
|
|
m[i] &^= 1 << j
|
|
m[j] &^= 1 << i
|
|
}
|
|
changed = true
|
|
}
|
|
bg := th.Widget
|
|
if ui.Hot() == id {
|
|
bg = th.WidgetHot
|
|
}
|
|
if on {
|
|
bg = th.Accent
|
|
}
|
|
ui.RoundRect(mathx.Rect{X: cell.X + 1, Y: cell.Y + 1, W: cell.W - 2, H: cell.H - 2}, th.Radius.Button, bg)
|
|
if on {
|
|
widgets.CheckMark(ui, cell, th.Background)
|
|
}
|
|
verb := " do not collide"
|
|
if on {
|
|
verb = " collide"
|
|
}
|
|
widgets.Tooltip(ui, id, names[i]+" and "+names[j]+verb)
|
|
}
|
|
}
|
|
if changed {
|
|
ps.SetMatrix(m)
|
|
}
|
|
return changed
|
|
}
|