179 lines
5.0 KiB
Go
179 lines
5.0 KiB
Go
package editor
|
|
|
|
import (
|
|
"math"
|
|
"strconv"
|
|
|
|
"aego/core/mathx"
|
|
"aego/modules/physics/world"
|
|
"aego/ui/core"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
const (
|
|
previewH = 190
|
|
pxPerM = 22
|
|
fixedStep = 1.0 / 60
|
|
)
|
|
|
|
func dot(ui *core.Context, p mathx.Vec2, s float32, c mathx.Color) {
|
|
ui.Rect(mathx.Rect{X: float32(int(p.X - s/2)), Y: float32(int(p.Y - s/2)), W: s, H: s}, c)
|
|
}
|
|
|
|
func line(ui *core.Context, a, b mathx.Vec2, w float32, c mathx.Color) {
|
|
d := b.Sub(a)
|
|
n := int(max(mathx.Abs(d.X), mathx.Abs(d.Y)))
|
|
if n == 0 {
|
|
dot(ui, a, w, c)
|
|
return
|
|
}
|
|
step := d.Scale(1 / float32(n))
|
|
for i := 0; i <= n; i++ {
|
|
dot(ui, a.Add(step.Scale(float32(i))), w, c)
|
|
}
|
|
}
|
|
|
|
func arrow(ui *core.Context, a, b mathx.Vec2, w float32, c mathx.Color) {
|
|
line(ui, a, b, w, c)
|
|
d := b.Sub(a)
|
|
l := d.Len()
|
|
if l < 1 {
|
|
return
|
|
}
|
|
u := d.Scale(1 / l)
|
|
n := mathx.Vec2{X: -u.Y, Y: u.X}
|
|
h := min(float32(8), l*0.4)
|
|
line(ui, b, b.Sub(u.Scale(h)).Add(n.Scale(h*0.6)), w, c)
|
|
line(ui, b, b.Sub(u.Scale(h)).Sub(n.Scale(h*0.6)), w, c)
|
|
}
|
|
|
|
func fmtF(v float32, prec int) string {
|
|
return strconv.FormatFloat(float64(v), 'f', prec, 32)
|
|
}
|
|
|
|
func (p *plugin) preview(ui *core.Context, cfg world.Config) {
|
|
th := &ui.Theme
|
|
r := ui.Cursor().Next(previewH)
|
|
ui.RoundRect(r, th.Radius.Card, th.Background)
|
|
ui.PushClip(r)
|
|
defer ui.PopClip()
|
|
pad := float32(10)
|
|
split := r.W * 0.58
|
|
scene := mathx.Rect{X: r.X + pad, Y: r.Y + pad, W: split - 2*pad, H: r.H - 2*pad}
|
|
spring := mathx.Rect{X: r.X + split + pad, Y: r.Y + pad, W: r.W - split - 2*pad, H: r.H - 2*pad}
|
|
ui.Rect(mathx.Rect{X: r.X + split, Y: r.Y + 8, W: 1, H: r.H - 16}, th.Separator)
|
|
p.trajectory(ui, scene, cfg)
|
|
p.contact(ui, spring, cfg)
|
|
}
|
|
|
|
func (p *plugin) trajectory(ui *core.Context, r mathx.Rect, cfg world.Config) {
|
|
th := &ui.Theme
|
|
g := cfg.Gravity
|
|
hot := p.focus == "gravity"
|
|
ground := r.Y + r.H - 36
|
|
ui.Rect(mathx.Rect{X: r.X, Y: ground, W: r.W, H: 1}, th.Border)
|
|
start := mathx.Vec2{X: r.X + 20, Y: ground - 2}
|
|
vel := mathx.Vec2{X: 5, Y: -7}
|
|
pos := mathx.Vec2{}
|
|
col := th.TextDim
|
|
if hot {
|
|
col = th.Accent
|
|
}
|
|
steps := 0
|
|
for i := 0; i < 240; i++ {
|
|
vel = vel.Add(g.Scale(fixedStep))
|
|
pos = pos.Add(vel.Scale(fixedStep))
|
|
sp := start.Add(pos.Scale(pxPerM))
|
|
if sp.Y > ground-2 || sp.X > r.X+r.W || sp.Y < r.Y-200 {
|
|
break
|
|
}
|
|
if sp.Y >= r.Y {
|
|
dot(ui, sp, 2, col)
|
|
}
|
|
steps++
|
|
}
|
|
ui.RoundRect(mathx.Rect{X: start.X - 6, Y: start.Y - 12, W: 12, H: 12}, 6, th.Accent)
|
|
arrow(ui, mathx.Vec2{X: start.X, Y: start.Y - 6}, mathx.Vec2{X: start.X + 5*4, Y: start.Y - 6 - 7*4}, 1, th.TextDim)
|
|
gl := g.Len()
|
|
c := mathx.Vec2{X: r.X + r.W - 46, Y: r.Y + 34}
|
|
if gl > 0 {
|
|
k := min(float32(40), 3*gl)
|
|
gc := th.AxisY
|
|
if hot {
|
|
gc = th.Accent
|
|
}
|
|
arrow(ui, c, c.Add(g.Scale(k/gl)), 2, gc)
|
|
}
|
|
lc := th.TextDim
|
|
if hot {
|
|
lc = th.Accent
|
|
}
|
|
ui.Text("Gravity "+fmtF(gl, 2)+" m/s²", mathx.Vec2{X: r.X, Y: r.Y}, lc)
|
|
info := "airtime " + fmtF(float32(steps)*fixedStep, 2) + " s · " + strconv.Itoa(steps) + " fixed steps · " + strconv.Itoa(cfg.Substeps) + " substeps"
|
|
sc := th.TextDim
|
|
if p.focus == "solver" {
|
|
sc = th.Accent
|
|
}
|
|
ui.Text(info, mathx.Vec2{X: r.X, Y: ground + 3}, sc)
|
|
sleep := "sleeps below " + fmtF(cfg.SleepLinear, 3) + " m/s for " + fmtF(cfg.SleepTime, 2) + " s"
|
|
slc := th.TextDisabled
|
|
if p.focus == "sleep" {
|
|
slc = th.Accent
|
|
}
|
|
ui.Text(sleep, mathx.Vec2{X: r.X, Y: ground + 3 + ui.LineHeight()}, slc)
|
|
}
|
|
|
|
func (p *plugin) contact(ui *core.Context, r mathx.Rect, cfg world.Config) {
|
|
th := &ui.Theme
|
|
hot := p.focus == "contact" || p.focus == "bounce"
|
|
title := "Contact " + fmtF(cfg.ContactHertz, 0) + " Hz · damping " + fmtF(cfg.ContactDampingRatio, 1)
|
|
ui.Text(title, mathx.Vec2{X: r.X, Y: r.Y}, th.TextDim)
|
|
plot := mathx.Rect{X: r.X, Y: r.Y + 22, W: r.W, H: r.H - 44}
|
|
mid := plot.Y + plot.H*0.5
|
|
ui.Rect(mathx.Rect{X: plot.X, Y: mid, W: plot.W, H: 1}, th.Border)
|
|
ui.Rect(mathx.Rect{X: plot.X, Y: plot.Y, W: 1, H: plot.H}, th.Border)
|
|
w := 2 * math.Pi * float64(max(cfg.ContactHertz, 0.1))
|
|
z := float64(max(cfg.ContactDampingRatio, 0))
|
|
window := 4 / float64(max(cfg.ContactHertz, 0.5))
|
|
if z > 1 {
|
|
window = max(window, 5/(w*(z-math.Sqrt(z*z-1))))
|
|
}
|
|
col := th.Text
|
|
if hot {
|
|
col = th.Accent
|
|
}
|
|
prev := mathx.Vec2{}
|
|
settle := -1.0
|
|
for i := 0; i <= int(plot.W); i++ {
|
|
t := window * float64(i) / float64(plot.W)
|
|
x := response(t, w, z)
|
|
if settle < 0 && math.Abs(x) < 0.02 {
|
|
settle = t
|
|
}
|
|
pt := mathx.Vec2{X: plot.X + float32(i), Y: mid - float32(x)*plot.H*0.45}
|
|
if i > 0 {
|
|
line(ui, prev, pt, 1, col)
|
|
}
|
|
prev = pt
|
|
}
|
|
foot := "penetration fades"
|
|
if settle >= 0 {
|
|
foot += " in " + fmtF(float32(settle*1000), 0) + " ms"
|
|
}
|
|
foot += " · push " + fmtF(cfg.ContactPushVelocity, 1) + " m/s"
|
|
ui.Text(widgets.Elide(ui, foot, r.W), mathx.Vec2{X: r.X, Y: plot.Y + plot.H + 4}, th.TextDim)
|
|
}
|
|
|
|
func response(t, w, z float64) float64 {
|
|
switch {
|
|
case z < 1:
|
|
wd := w * math.Sqrt(1-z*z)
|
|
return math.Exp(-z*w*t) * (math.Cos(wd*t) + z/math.Sqrt(1-z*z)*math.Sin(wd*t))
|
|
case z == 1:
|
|
return math.Exp(-w*t) * (1 + w*t)
|
|
}
|
|
s := math.Sqrt(z*z - 1)
|
|
r1, r2 := -w*(z-s), -w*(z+s)
|
|
return (r2*math.Exp(r1*t) - r1*math.Exp(r2*t)) / (r2 - r1)
|
|
}
|