489 lines
10 KiB
Go
489 lines
10 KiB
Go
package debug
|
|
|
|
import (
|
|
"math"
|
|
"strings"
|
|
|
|
"aego/contracts/debugdraw"
|
|
"aego/core/mathx"
|
|
"aego/engine"
|
|
"aego/render"
|
|
rdebug "aego/render/debug"
|
|
"aego/render/text"
|
|
"aego/scene"
|
|
"aego/script/vm"
|
|
)
|
|
|
|
type kind uint8
|
|
|
|
const (
|
|
kLine kind = iota
|
|
kArrow
|
|
kCircle
|
|
kRect
|
|
kPoly
|
|
kCross
|
|
kPoint
|
|
kText
|
|
)
|
|
|
|
const (
|
|
defaultChannel = "default"
|
|
scriptChannel = "script"
|
|
maxChannels = 1024
|
|
)
|
|
|
|
type prim struct {
|
|
kind kind
|
|
closed bool
|
|
gone bool
|
|
space debugdraw.Space
|
|
layer debugdraw.Layer
|
|
fade debugdraw.Fade
|
|
life debugdraw.LifeKind
|
|
dom debugdraw.Domain
|
|
ch uint16
|
|
pts int32
|
|
n int32
|
|
id debugdraw.Handle
|
|
a, b mathx.Vec2
|
|
r float32
|
|
thick float32
|
|
left float32
|
|
total float32
|
|
color mathx.Color
|
|
owner scene.NodeHandle
|
|
text string
|
|
src string
|
|
}
|
|
|
|
type channel struct {
|
|
name string
|
|
count int
|
|
dropped uint64
|
|
state int8
|
|
}
|
|
|
|
type pen struct {
|
|
ch string
|
|
life debugdraw.Lifetime
|
|
}
|
|
|
|
type Service struct {
|
|
cfg Settings
|
|
prims []prim
|
|
pts []mathx.Vec2
|
|
spare []mathx.Vec2
|
|
chans []channel
|
|
byName map[string]uint16
|
|
hidden []string
|
|
next debugdraw.Handle
|
|
dropped uint64
|
|
pens map[vm.InstanceID]pen
|
|
def debugdraw.Style
|
|
tmp debugdraw.Style
|
|
}
|
|
|
|
var _ debugdraw.Draw = (*Service)(nil)
|
|
|
|
func NewService(cfg Settings) *Service {
|
|
cfg = cfg.normalized()
|
|
n := int(cfg.MaxPrimitives)
|
|
s := &Service{
|
|
cfg: cfg,
|
|
prims: make([]prim, 0, n),
|
|
pts: make([]mathx.Vec2, 0, 2*n),
|
|
spare: make([]mathx.Vec2, 0, 2*n),
|
|
byName: map[string]uint16{},
|
|
def: debugdraw.Style{Color: mathx.Color{R: 1, G: 1, B: 1, A: 1}, Channel: defaultChannel},
|
|
}
|
|
s.channel(defaultChannel)
|
|
return s
|
|
}
|
|
|
|
func Of(w *engine.World) *Service {
|
|
s, _ := engine.ServiceOf[*Service](w, debugdraw.ServiceID)
|
|
return s
|
|
}
|
|
|
|
func (s *Service) Enabled() bool {
|
|
return s.cfg.Enabled
|
|
}
|
|
|
|
func (s *Service) channel(name string) uint16 {
|
|
if name == "" {
|
|
name = defaultChannel
|
|
}
|
|
if i, ok := s.byName[name]; ok {
|
|
return i
|
|
}
|
|
if len(s.chans) >= maxChannels {
|
|
return 0
|
|
}
|
|
i := uint16(len(s.chans))
|
|
s.chans = append(s.chans, channel{name: name})
|
|
s.byName[name] = i
|
|
return i
|
|
}
|
|
|
|
func (s *Service) add(k kind, a, b mathx.Vec2, r float32, st *debugdraw.Style) *prim {
|
|
if !s.cfg.Enabled {
|
|
return nil
|
|
}
|
|
if st == nil {
|
|
st = &s.def
|
|
}
|
|
ci := s.channel(st.Channel)
|
|
c := &s.chans[ci]
|
|
if len(s.prims) == cap(s.prims) || c.count >= int(s.cfg.MaxPerChannel) {
|
|
c.dropped++
|
|
s.dropped++
|
|
return nil
|
|
}
|
|
col := st.Color
|
|
if col == (mathx.Color{}) {
|
|
col = s.def.Color
|
|
}
|
|
s.next++
|
|
s.prims = append(s.prims, prim{
|
|
kind: k, space: st.Space, layer: st.Layer, fade: st.Fade, life: st.Life.Kind, dom: st.Life.Domain,
|
|
ch: ci, id: s.next, a: a, b: b, r: r, thick: st.Thickness, left: st.Life.N, total: st.Life.N,
|
|
color: col, owner: st.Owner, src: st.Source,
|
|
})
|
|
c.count++
|
|
return &s.prims[len(s.prims)-1]
|
|
}
|
|
|
|
func id(p *prim) debugdraw.Handle {
|
|
if p == nil {
|
|
return 0
|
|
}
|
|
return p.id
|
|
}
|
|
|
|
func (s *Service) Line(a, b mathx.Vec2, st *debugdraw.Style) debugdraw.Handle {
|
|
return id(s.add(kLine, a, b, 0, st))
|
|
}
|
|
|
|
func (s *Service) Arrow(from, to mathx.Vec2, st *debugdraw.Style) debugdraw.Handle {
|
|
return id(s.add(kArrow, from, to, 0, st))
|
|
}
|
|
|
|
func (s *Service) Ray(origin, dir mathx.Vec2, st *debugdraw.Style) debugdraw.Handle {
|
|
return id(s.add(kArrow, origin, origin.Add(dir), 0, st))
|
|
}
|
|
|
|
func (s *Service) Circle(center mathx.Vec2, r float32, st *debugdraw.Style) debugdraw.Handle {
|
|
return id(s.add(kCircle, center, center, r, st))
|
|
}
|
|
|
|
func (s *Service) Rect(r mathx.Rect, st *debugdraw.Style) debugdraw.Handle {
|
|
return id(s.add(kRect, mathx.Vec2{X: r.X, Y: r.Y}, mathx.Vec2{X: r.X + r.W, Y: r.Y + r.H}, 0, st))
|
|
}
|
|
|
|
func (s *Service) AABB(lo, hi mathx.Vec2, st *debugdraw.Style) debugdraw.Handle {
|
|
return id(s.add(kRect, lo, hi, 0, st))
|
|
}
|
|
|
|
func (s *Service) Cross(p mathx.Vec2, size float32, st *debugdraw.Style) debugdraw.Handle {
|
|
return id(s.add(kCross, p, p, size, st))
|
|
}
|
|
|
|
func (s *Service) Point(p mathx.Vec2, st *debugdraw.Style) debugdraw.Handle {
|
|
return id(s.add(kPoint, p, p, 0, st))
|
|
}
|
|
|
|
func (s *Service) Text(p mathx.Vec2, t string, st *debugdraw.Style) debugdraw.Handle {
|
|
pr := s.add(kText, p, p, 0, st)
|
|
if pr == nil {
|
|
return 0
|
|
}
|
|
pr.text = t
|
|
return pr.id
|
|
}
|
|
|
|
func (s *Service) Polygon(pts []mathx.Vec2, closed bool, st *debugdraw.Style) debugdraw.Handle {
|
|
if !s.cfg.Enabled || len(pts) < 2 {
|
|
return 0
|
|
}
|
|
if len(s.pts)+len(pts) > cap(s.pts) {
|
|
s.dropped++
|
|
return 0
|
|
}
|
|
p := s.add(kPoly, mathx.Vec2{}, mathx.Vec2{}, 0, st)
|
|
if p == nil {
|
|
return 0
|
|
}
|
|
p.pts, p.n, p.closed = int32(len(s.pts)), int32(len(pts)), closed
|
|
s.pts = append(s.pts, pts...)
|
|
return p.id
|
|
}
|
|
|
|
func (s *Service) kill(p *prim) {
|
|
if !p.gone {
|
|
p.gone = true
|
|
s.chans[p.ch].count--
|
|
}
|
|
}
|
|
|
|
func (s *Service) Clear(h debugdraw.Handle) {
|
|
for i := range s.prims {
|
|
if s.prims[i].id == h {
|
|
s.kill(&s.prims[i])
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) ClearChannel(prefix string) {
|
|
for i := range s.prims {
|
|
if strings.HasPrefix(s.chans[s.prims[i].ch].name, prefix) {
|
|
s.kill(&s.prims[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) visible(ci uint16) bool {
|
|
switch s.chans[ci].state {
|
|
case 1:
|
|
return true
|
|
case -1:
|
|
return false
|
|
}
|
|
name := s.chans[ci].name
|
|
for _, p := range s.hidden {
|
|
if strings.HasPrefix(name, p) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *Service) Wants(name string) bool {
|
|
return s.cfg.Enabled && s.visible(s.channel(name))
|
|
}
|
|
|
|
func (s *Service) Hide(name string, hidden bool) {
|
|
c := &s.chans[s.channel(name)]
|
|
c.state = 1
|
|
if hidden {
|
|
c.state = -1
|
|
}
|
|
}
|
|
|
|
func (s *Service) HideByDefault(prefix string) {
|
|
for _, p := range s.hidden {
|
|
if p == prefix {
|
|
return
|
|
}
|
|
}
|
|
s.hidden = append(s.hidden, prefix)
|
|
}
|
|
|
|
func (p *prim) alive(paused bool, game, real float64) bool {
|
|
if p.gone {
|
|
return false
|
|
}
|
|
switch p.life {
|
|
case debugdraw.OneFrame:
|
|
return paused
|
|
case debugdraw.Frames:
|
|
if paused {
|
|
return true
|
|
}
|
|
p.left--
|
|
return p.left > 0
|
|
case debugdraw.Seconds:
|
|
d := game
|
|
if p.dom == debugdraw.Unscaled {
|
|
d = real
|
|
}
|
|
p.left -= float32(d)
|
|
return p.left > 0
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *Service) Advance(paused bool, game, real float64) {
|
|
w := 0
|
|
s.spare = s.spare[:0]
|
|
for i := range s.prims {
|
|
p := s.prims[i]
|
|
gone := p.gone
|
|
if !p.alive(paused, game, real) {
|
|
if !gone {
|
|
s.chans[p.ch].count--
|
|
}
|
|
continue
|
|
}
|
|
if p.kind == kPoly {
|
|
at := len(s.spare)
|
|
s.spare = append(s.spare, s.pts[p.pts:p.pts+p.n]...)
|
|
p.pts = int32(at)
|
|
}
|
|
s.prims[w] = p
|
|
w++
|
|
}
|
|
clear(s.prims[w:])
|
|
s.prims = s.prims[:w]
|
|
s.pts, s.spare = s.spare, s.pts[:0]
|
|
}
|
|
|
|
func (s *Service) Live() int {
|
|
n := 0
|
|
for i := range s.prims {
|
|
if !s.prims[i].gone {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func (s *Service) Dropped() uint64 {
|
|
return s.dropped
|
|
}
|
|
|
|
func (s *Service) EachChannel(fn func(name string, count int, dropped uint64)) {
|
|
for _, c := range s.chans {
|
|
fn(c.name, c.count, c.dropped)
|
|
}
|
|
}
|
|
|
|
func (s *Service) SetPen(owner vm.InstanceID, ch string, ttl float32, unscaled bool) {
|
|
if s.pens == nil {
|
|
s.pens = map[vm.InstanceID]pen{}
|
|
}
|
|
var life debugdraw.Lifetime
|
|
switch {
|
|
case ttl < 0:
|
|
life = debugdraw.Forever()
|
|
case ttl > 0 && unscaled:
|
|
life = debugdraw.ForUnscaled(ttl)
|
|
case ttl > 0:
|
|
life = debugdraw.ForSeconds(ttl)
|
|
}
|
|
s.pens[owner] = pen{ch: ch, life: life}
|
|
}
|
|
|
|
func (s *Service) scriptStyle(owner vm.InstanceID, node scene.NodeHandle, col mathx.Color) *debugdraw.Style {
|
|
p := s.pens[owner]
|
|
if p.ch == "" {
|
|
p.ch = scriptChannel
|
|
}
|
|
s.tmp = debugdraw.Style{Color: col, Life: p.life, Channel: p.ch, Owner: node, Source: scriptChannel}
|
|
return &s.tmp
|
|
}
|
|
|
|
func (p *prim) factor() float32 {
|
|
if p.total <= 0 || (p.life != debugdraw.Seconds && p.life != debugdraw.Frames) {
|
|
return 1
|
|
}
|
|
return mathx.Clamp(p.left/p.total, 0, 1)
|
|
}
|
|
|
|
func (p *prim) tint() (mathx.Color, float32) {
|
|
f := p.factor()
|
|
c, k := p.color, float32(1)
|
|
if p.fade != debugdraw.FadeNone {
|
|
c.A *= f
|
|
}
|
|
if p.fade == debugdraw.FadeAlphaScale {
|
|
k = f
|
|
}
|
|
return c, k
|
|
}
|
|
|
|
func (s *Service) Lines(d *rdebug.Draw, px, scale float32, screen bool) {
|
|
if scale <= 0 {
|
|
scale = 1
|
|
}
|
|
for i := range s.prims {
|
|
p := &s.prims[i]
|
|
if p.gone || p.kind == kText || (p.space == debugdraw.Screen && !screen) || !s.visible(p.ch) {
|
|
continue
|
|
}
|
|
c, k := p.tint()
|
|
phys, unit := px, px*scale
|
|
switch {
|
|
case p.space == debugdraw.Screen:
|
|
d.Space(rdebug.Screen)
|
|
phys, unit = 1/scale, 1
|
|
case p.layer == debugdraw.Overlay:
|
|
d.Space(rdebug.Overlay)
|
|
default:
|
|
d.Space(rdebug.World)
|
|
}
|
|
n, step := 1, phys
|
|
if p.thick > 1 && p.kind <= kRect {
|
|
w, limit := p.thick*scale, 8
|
|
if p.kind == kCircle {
|
|
w, step, limit = w*math.Sqrt2, phys/math.Sqrt2, 12
|
|
}
|
|
n = min(int(w+0.5), limit)
|
|
}
|
|
for t := range n {
|
|
s.shape(d, p, c, k, unit, (float32(t)-float32(n-1)/2)*step)
|
|
}
|
|
}
|
|
d.Space(rdebug.World)
|
|
}
|
|
|
|
func (s *Service) shape(d *rdebug.Draw, p *prim, c mathx.Color, k, unit, off float32) {
|
|
switch p.kind {
|
|
case kLine, kArrow:
|
|
v := p.b.Sub(p.a).Scale(k)
|
|
l := v.Len()
|
|
if l == 0 {
|
|
return
|
|
}
|
|
u := v.Scale(1 / l)
|
|
o := mathx.Vec2{Y: off}
|
|
if mathx.Abs(u.X) < mathx.Abs(u.Y) {
|
|
o = mathx.Vec2{X: off}
|
|
}
|
|
a, b := p.a.Add(o), p.a.Add(v).Add(o)
|
|
d.Line(a, b, c)
|
|
if p.kind == kArrow {
|
|
h := min(10*unit, l*0.4)
|
|
d.Line(b, b.Sub(u.Rotated(0.45).Scale(h)), c)
|
|
d.Line(b, b.Sub(u.Rotated(-0.45).Scale(h)), c)
|
|
}
|
|
case kCircle:
|
|
d.Circle(p.a, max(p.r*k+off, 0), 32, c)
|
|
case kRect:
|
|
e := mathx.Vec2{X: off, Y: off}
|
|
d.AABB(mathx.AABB2{Min: p.a.Sub(e), Max: p.b.Add(e)}, c)
|
|
case kPoly:
|
|
pts := s.pts[p.pts : p.pts+p.n]
|
|
for i := 0; i+1 < len(pts); i++ {
|
|
d.Line(pts[i], pts[i+1], c)
|
|
}
|
|
if p.closed && len(pts) > 2 {
|
|
d.Line(pts[len(pts)-1], pts[0], c)
|
|
}
|
|
case kCross:
|
|
d.Cross(p.a, p.r*k, c)
|
|
case kPoint:
|
|
d.Cross(p.a, 6*unit*k, c)
|
|
}
|
|
}
|
|
|
|
func (s *Service) Texts(f *render.ViewFrame, font *text.Font, px float32) {
|
|
if font == nil {
|
|
return
|
|
}
|
|
v := f.View()
|
|
for i := range s.prims {
|
|
p := &s.prims[i]
|
|
if p.gone || p.kind != kText || p.text == "" || !s.visible(p.ch) {
|
|
continue
|
|
}
|
|
c, k := p.tint()
|
|
pos := p.a
|
|
if p.space == debugdraw.Screen {
|
|
pos = v.ScreenToWorld(p.a.Scale(v.PixelScale))
|
|
}
|
|
f.TextFont(font, p.text, pos, px*v.PixelScale*k, c, math.MaxInt16, 0)
|
|
}
|
|
}
|