Files

412 lines
9.2 KiB
Go

package sceneview
import (
"math"
"aego/core/ids"
"aego/core/mathx"
"aego/editor/core/gizmo"
"aego/editor/core/props"
"aego/editor/core/shape"
"aego/render/debug"
"aego/render/view"
"aego/scene"
)
type Host3 interface {
Scene() *scene.Scene
Shapes() shape.Set
Selected() []ids.NodeID
Select(nodes []ids.NodeID, additive bool)
Commit3D(label string, edits []props.Transform3DEdit)
Tool3() gizmo.Op3
ShowGizmo() bool
LocalAxes() bool
Snap3() gizmo.Snap3
}
const (
ButtonLeft = iota
ButtonRight
ButtonMiddle
)
const (
clickSlopPx float32 = 4
pointPickPx float32 = 10
axesSizePx float32 = 80
axesMargin float32 = 8
)
type Input3 struct {
Mouse mathx.Vec2
Delta mathx.Vec2
Wheel float32
Down [3]bool
Pressed [3]bool
Hovered bool
Alt bool
Shift bool
Frame bool
Cancel bool
}
type Theme3 struct {
Selection mathx.Color
Camera mathx.Color
Box mathx.Color
Grid mathx.Color
Gizmo gizmo.Colors3
}
type nav3 uint8
const (
navNone nav3 = iota
navOrbit
navPan
)
type edit3 struct {
drag gizmo.Drag3
nodes []ids.NodeID
parents []mathx.Mat4
before []scene.Transform3D
}
type Controller3 struct {
Cam Camera3D
vp mathx.Rect
hot gizmo.Part3
nav nav3
edit *edit3
pressing bool
boxing bool
pressAt mathx.Vec2
mouse mathx.Vec2
mask uint64
}
func NewController3() *Controller3 {
return &Controller3{Cam: DefaultCamera3D(), vp: mathx.Rect{W: 1, H: 1}, mask: ^uint64(0)}
}
func (c *Controller3) SetViewport(w, h float32) {
c.vp = mathx.Rect{W: max(w, 1), H: max(h, 1)}
}
func (c *Controller3) View() view.View {
return c.Cam.View(c.vp)
}
func (c *Controller3) Ray(p mathx.Vec2) mathx.Ray3 {
return c.View().ScreenToRay(p)
}
func (c *Controller3) Hot() gizmo.Part3 {
return c.hot
}
func (c *Controller3) Busy() bool {
return c.edit != nil || c.nav != navNone || c.pressing
}
func ancestorIn(s *scene.Scene, h scene.NodeHandle, set []ids.NodeID) bool {
for p := s.Parent(h); !p.IsZero() && p != s.Root(); p = s.Parent(p) {
id := s.ID(p)
for _, x := range set {
if x == id {
return true
}
}
}
return false
}
func editable(s *scene.Scene, selected []ids.NodeID) []ids.NodeID {
out := make([]ids.NodeID, 0, len(selected))
for _, id := range selected {
h := s.Handle(id)
if h.IsZero() || !s.IsAlive(h) || s.Space(h) != scene.Space3D || ancestorIn(s, h, selected) {
continue
}
out = append(out, id)
}
return out
}
func (c *Controller3) gizmoFrame(h Host3) (gizmo.Frame3, bool) {
if !h.ShowGizmo() {
return gizmo.Frame3{}, false
}
s := h.Scene()
nodes := editable(s, h.Selected())
if len(nodes) == 0 {
return gizmo.Frame3{}, false
}
var sum mathx.Vec3
for _, id := range nodes {
sum = sum.Add(s.WorldPosition3D(s.Handle(id)))
}
f := gizmo.Frame3{View: c.View(), Pivot: sum.Scale(1 / float32(len(nodes))), Rotation: mathx.QuatIdentity()}
if h.LocalAxes() || h.Tool3() == gizmo.Scale3 {
if _, rot, _, ok := s.World4(s.Handle(nodes[len(nodes)-1])).Decompose(); ok {
f.Rotation = rot
}
}
return f, true
}
func (c *Controller3) Update(h Host3, in Input3) bool {
c.mouse = in.Mouse
if c.edit != nil {
return c.updateEdit(h, in)
}
if c.nav != navNone {
if !in.Down[ButtonLeft] && !in.Down[ButtonRight] && !in.Down[ButtonMiddle] {
c.nav = navNone
return true
}
if c.nav == navOrbit {
c.Cam.Orbit(in.Delta.X, in.Delta.Y)
} else {
c.Cam.Pan(in.Delta.X, in.Delta.Y, c.vp.H)
}
return true
}
if c.pressing {
return c.updatePress(h, in)
}
if !in.Hovered {
c.hot = gizmo.Part3None
return false
}
consumed := false
if in.Wheel != 0 {
c.Cam.Dolly(in.Wheel)
consumed = true
}
if in.Frame {
c.FrameSelection(h)
consumed = true
}
switch {
case in.Pressed[ButtonRight] || (in.Pressed[ButtonLeft] && in.Alt):
c.nav = navOrbit
return true
case in.Pressed[ButtonMiddle]:
c.nav = navPan
return true
}
frame, ok := c.gizmoFrame(h)
c.hot = gizmo.Part3None
if ok {
c.hot = gizmo.Hit3(h.Tool3(), frame, c.Ray(in.Mouse))
}
if in.Pressed[ButtonLeft] {
if c.hot != gizmo.Part3None && c.beginEdit(h, frame, in) {
return true
}
c.pressing = true
c.pressAt = in.Mouse
return true
}
return consumed
}
func (c *Controller3) beginEdit(h Host3, f gizmo.Frame3, in Input3) bool {
d, ok := gizmo.BeginDrag3(h.Tool3(), c.hot, f, c.Ray(in.Mouse))
if !ok {
return false
}
s := h.Scene()
e := &edit3{drag: d}
for _, id := range editable(s, h.Selected()) {
n := s.Handle(id)
e.nodes = append(e.nodes, id)
e.parents = append(e.parents, s.World4(s.Parent(n)))
e.before = append(e.before, s.Local3D(n))
}
c.edit = e
return true
}
func (c *Controller3) restore(s *scene.Scene) {
for i, id := range c.edit.nodes {
if n := s.Handle(id); !n.IsZero() {
s.SetLocal3D(n, c.edit.before[i])
}
}
}
func (c *Controller3) updateEdit(h Host3, in Input3) bool {
e := c.edit
s := h.Scene()
if in.Cancel {
c.restore(s)
c.edit = nil
return true
}
if delta, ok := e.drag.Update(c.Ray(in.Mouse), h.Snap3()); ok {
for i, id := range e.nodes {
if n := s.Handle(id); !n.IsZero() {
s.SetLocal3D(n, gizmo.ApplyDelta3(e.parents[i], e.before[i], e.drag.Pivot, delta))
}
}
}
if in.Down[ButtonLeft] {
return true
}
edits := make([]props.Transform3DEdit, 0, len(e.nodes))
for i, id := range e.nodes {
n := s.Handle(id)
if n.IsZero() {
continue
}
after := s.Local3D(n)
if after != e.before[i] {
edits = append(edits, props.Transform3DEdit{Node: id, Before: e.before[i], After: after})
}
}
c.restore(s)
c.edit = nil
if len(edits) > 0 {
h.Commit3D(e.drag.Op.Label(), edits)
}
return true
}
func (c *Controller3) updatePress(h Host3, in Input3) bool {
dx, dy := in.Mouse.X-c.pressAt.X, in.Mouse.Y-c.pressAt.Y
if !c.boxing && mathx.Sqrt(dx*dx+dy*dy) > clickSlopPx {
c.boxing = true
}
if in.Cancel {
c.pressing, c.boxing = false, false
return true
}
if in.Down[ButtonLeft] {
return true
}
c.pressing = false
if c.boxing {
c.boxing = false
h.Select(BoxSelect3D(h.Scene(), h.Shapes(), c.View(), c.pressAt, in.Mouse, c.mask), in.Shift)
return true
}
if n, ok := c.PickAt(h, in.Mouse); ok {
h.Select([]ids.NodeID{h.Scene().ID(n)}, in.Shift)
} else if !in.Shift {
h.Select(nil, false)
}
return true
}
func (c *Controller3) PickAt(h Host3, p mathx.Vec2) (scene.NodeHandle, bool) {
s := h.Scene()
ray := c.Ray(p)
v := c.View()
best, bestT, ok := Pick3D(s, h.Shapes(), ray, c.mask)
radius := func(at mathx.Vec3) float32 {
return gizmo.PxToWorld(v, at, pointPickPx)
}
if pt, t, pok := PickPoint3D(s, h.Shapes(), ray, c.mask, radius); pok && (!ok || t < bestT) {
return pt, true
}
return best, ok
}
func (c *Controller3) FrameSelection(h Host3) {
s := h.Scene()
b, ok := SelectionBounds(s, h.Shapes(), h.Selected())
if !ok {
b, ok = SceneBounds(s, h.Shapes())
}
if !ok {
b = mathx.AABB3{Min: mathx.Vec3{X: -1, Y: -1, Z: -1}, Max: mathx.Vec3{X: 1, Y: 1, Z: 1}}
}
c.Cam.Frame(b)
}
func (c *Controller3) DropPoint(h Host3, p mathx.Vec2, snap float32) mathx.Vec3 {
ray := c.Ray(p)
var at mathx.Vec3
if _, t, ok := Pick3D(h.Scene(), h.Shapes(), ray, c.mask); ok {
at = ray.At(t)
} else if t, ok := intersectPlane(ray, mathx.Vec3{}, mathx.Vec3{Y: 1}); ok {
at = ray.At(t)
} else {
at = c.Cam.Target
}
if snap > 0 {
at.X = snapTo(at.X, snap)
at.Z = snapTo(at.Z, snap)
}
return at
}
func snapTo(v, step float32) float32 {
if step <= 0 {
return v
}
return float32(math.Round(float64(v/step))) * step
}
func (c *Controller3) AxesView(base view.View) view.View {
cam := Camera3D{Distance: 4, Yaw: c.Cam.Yaw, Pitch: c.Cam.Pitch, FOV: 30}
v := cam.View(mathx.Rect{X: axesMargin, Y: c.vp.H - axesSizePx - axesMargin, W: axesSizePx, H: axesSizePx})
v.Target, v.TargetW, v.TargetH = base.Target, base.TargetW, base.TargetH
v.PixelScale = base.PixelScale
v.DoClear = false
v.Label = "sceneview3d.axes"
return v
}
func DrawGrid3(dbg *debug.Draw, cam Camera3D, step float32, th Theme3) {
if step <= 0 {
step = 1
}
for cam.dist()/step > 40 {
step *= 10
}
const count = 50
center := mathx.Vec3{X: snapTo(cam.Target.X, step*10), Z: snapTo(cam.Target.Z, step*10)}
dbg.Grid3(center, step, count, th.Grid)
ext := step * count
dbg.Line3(mathx.Vec3{X: center.X - ext}, mathx.Vec3{X: center.X + ext}, th.Gizmo.X)
dbg.Line3(mathx.Vec3{Z: center.Z - ext}, mathx.Vec3{Z: center.Z + ext}, th.Gizmo.Z)
}
func (c *Controller3) DrawOverlay(h Host3, dbg *debug.Draw, th Theme3) {
s := h.Scene()
v := c.View()
dbg.Space(debug.Overlay)
sh := h.Shapes()
sh.Overlays(&shape.OverlayCtx{Scene: s, Debug: dbg, View: v, Mode3D: true, Aspect: c.vp.W / c.vp.H, Color: th.Camera})
for _, id := range h.Selected() {
n := s.Handle(id)
if n.IsZero() || !s.IsAlive(n) {
continue
}
if b, ok := sh.Of(s, n); ok && b.Kind == shape.Box {
dbg.AABB3(b.Box, th.Selection)
continue
}
dbg.Axes3(s.World4(n), gizmo.PxToWorld(v, s.WorldPosition3D(n), 40))
}
if f, ok := c.gizmoFrame(h); ok {
dc := &gizmo.DrawCtx3{Debug: dbg, Frame: f, Colors: th.Gizmo, Hot: c.hot}
if c.edit != nil {
dc.Active = c.edit.drag.Part
}
gizmo.Draw3(h.Tool3(), dc)
}
if c.boxing {
dbg.Space(debug.Screen)
dbg.AABB(mathx.AABB2{
Min: mathx.Vec2{X: min(c.pressAt.X, c.mouse.X), Y: min(c.pressAt.Y, c.mouse.Y)},
Max: mathx.Vec2{X: max(c.pressAt.X, c.mouse.X), Y: max(c.pressAt.Y, c.mouse.Y)},
}, th.Box)
}
}