Files
aego/editor/panels/sceneview/panel.go
sanspie 4920c2a11c release: prepare 0.12.1 and fix CI checks
Prepare the stable editor update and fix CI checks for module icons, diagnostics, updater retries and integration snapshots.
2026-11-05 22:28:27 +03:00

1097 lines
27 KiB
Go

package sceneview
import (
"aego/editor/core/follow"
"aego/editor/core/presence"
"aego/editor/core/selection"
wpresence "aego/ui/widgets/presence"
"math"
"path"
"strconv"
"strings"
"aego/asset"
"aego/asset/pipeline"
"aego/core/ids"
"aego/core/mathx"
"aego/editor/core/assetfiles"
"aego/editor/core/clipboard"
"aego/editor/core/command"
"aego/editor/core/context"
"aego/editor/core/document"
"aego/editor/core/gizmo"
"aego/editor/core/services"
"aego/editor/core/shape"
"aego/editor/core/templates"
"aego/editor/core/vectors"
"aego/editor/core/workspace"
"aego/engine"
"aego/engine/i18n"
"aego/gpu"
"aego/platform/input"
"aego/project"
"aego/render/debug"
"aego/render/rt"
"aego/render/view"
"aego/scene"
"aego/scene/frame"
"aego/ui/core"
"aego/ui/widgets"
)
type mode uint8
const (
modeIdle mode = iota
modePan
modeGizmo
modeBox
)
type world struct {
w *engine.World
runner *frame.Runner
scene *scene.Scene
}
type Deps struct {
Preview func(*frame.Runner)
Capture func(tex gpu.TextureHandle, w, h int, name string)
Viewport *services.Viewport
Resolver asset.Resolver
Docs services.Documents
Selection services.Selection
Tools *context.SceneSettings
Commands *command.Registry
Notices services.Notices
Scenes services.SceneOps
Files *assetfiles.Files
Pipeline *pipeline.Pipeline
Project *project.Project
Templates *templates.Registry
// Dragging tells animation preview that the transform gizmo starts or ends a drag (D337).
Dragging func(on bool)
// Presence and AgentSel draw where the agent works: its marker, its selection and what it removed (11b5b).
Presence *presence.Model
AgentSel *selection.Service
}
type Panel struct {
capturedScene *scene.Scene
capturedTarget *rt.Target
capturedTick uint64
glide glide
agentNodes []ids.NodeID
pressAt mathx.Vec2
moved bool
d Deps
iso isolation
isoVer uint64
captureReq bool
f *services.Frame
hook func()
pool *rt.Pool
target *rt.Target
worlds map[document.ID]*world
cur *world
rect mathx.Rect
scale float32
view view.View
space gizmo.Space
mode mode
viewMode Mode
c3 *Controller3
c3Doc document.ID
gizmo gizmo.Transform
gizmos *gizmo.Registry
active gizmo.Gizmo
edit *gizmo.EditCtx
hot gizmo.Part
hotGizmo gizmo.Gizmo
boxStart mathx.Vec2
boxEnd mathx.Vec2
boxAdd bool
pivot mathx.Vec2
rotation float32
activeNode ids.NodeID
hasPivot bool
title string
titleKey titleKey
icon string
barRect mathx.Rect
menuID core.ID
menuAt mathx.Vec2
menuAt3 mathx.Vec3
panRight bool
panMoved bool
panStart mathx.Vec2
rightAt mathx.Vec2
rightArmed bool
}
func New(g *gizmo.Registry, d Deps) *Panel {
return &Panel{d: d, worlds: map[document.ID]*world{}, gizmos: g, c3: NewController3()}
}
func (p *Panel) ui() *core.Context {
if p.f != nil {
return p.f.UI
}
return p.d.Viewport.UI
}
func (p *Panel) shapes() shape.Set {
return shape.Set{Reg: p.d.Viewport.Shapes, Res: p.d.Resolver, Hide: p.hidden}
}
func (p *Panel) docs() *document.Manager {
return p.d.Docs.Documents()
}
func (p *Panel) item(id string, label string) {
command.MenuItem(p.ui(), p.d.Commands, id, label)
}
func (p *Panel) ID() string {
return workspace.Scene
}
type titleKey struct {
path string
dirty bool
play bool
}
func (p *Panel) Title() string {
if p.title == "" {
return i18n.Tr("editor.sceneview.title", "Scene")
}
return p.title
}
func (p *Panel) updateTitle(d *document.SceneDocument) {
k := titleKey{path: d.Path, play: p.docs().InPlay()}
if real := p.docs().Get(d.ID); real != nil {
k.dirty = real.Dirty()
}
if k == p.titleKey && p.title != "" {
return
}
p.titleKey = k
t := strings.TrimSuffix(path.Base(d.Path), path.Ext(d.Path))
if k.play {
t += " [Play]"
}
p.title = t
p.icon = "file.scene"
if d.IsPrefab() {
p.icon = "file.prefab"
}
}
func (p *Panel) Icon() string {
if p.icon == "" {
return "file.scene"
}
return p.icon
}
func (p *Panel) Dirty() bool {
return p.titleKey.dirty
}
func (p *Panel) ZoomLabel() string {
d := p.d.Docs.Doc()
if d == nil {
return "—"
}
z := d.View.Camera.Zoom
if z <= 0 {
z = 64
}
return strconv.Itoa(int(z/64*100+0.5)) + "%"
}
func (p *Panel) SetZoom(z float32) {
if d := p.d.Docs.Doc(); d != nil {
d.View.Camera.Zoom = z
}
}
func (p *Panel) Scope() string {
return command.ScopeScene
}
func (p *Panel) ensure() {
if p.pool != nil {
return
}
p.hook = p.render
p.pool = rt.New(p.d.Viewport.GPU, gpu.TexSRGBA8, "editor.sceneview")
}
func (p *Panel) Close() {
if p.pool != nil {
if p.target != nil {
p.pool.Release(p.target)
p.target = nil
}
p.pool.Close()
}
p.worlds = map[document.ID]*world{}
}
func (p *Panel) prune() {
for id := range p.worlds {
if p.docs().Get(id) == nil {
delete(p.worlds, id)
}
}
}
func (p *Panel) worldFor(d *document.SceneDocument) *world {
wd := p.worlds[d.ID]
if wd == nil {
wd = &world{w: engine.NewWorld("edit"), runner: frame.New()}
wd.runner.SetFixedBackdrop(p.drawFixedBackdrop)
wd.runner.SetPreviewExtract(p.d.Preview)
wd.w.SetService(frame.ServiceID, wd.runner)
p.worlds[d.ID] = wd
}
if wd.scene != d.Scene() {
wd.scene = d.Scene()
wd.runner.Bind(wd.scene, p.d.Viewport.Renderer, p.d.Resolver)
wd.w.SetService(scene.ServiceID, wd.scene)
}
return wd
}
func (p *Panel) refreshView(d *document.SceneDocument) {
cam := d.View.Camera
p.view = view.View{
Camera: view.Camera2D{Position: cam.Position, Zoom: cam.Zoom * p.scale},
Viewport: mathx.Rect{W: float32(p.target.W), H: float32(p.target.H)},
Target: p.target.Texture,
TargetW: p.target.BucketW,
TargetH: p.target.BucketH,
PixelScale: p.scale,
PixelsPerUnit: 1,
ZMax: 1,
Clear: p.ui().Theme.SceneBg,
DoClear: true,
Layers: view.AllLayers,
Label: "sceneview",
}
p.space = gizmo.Space{View: p.view, Zoom: cam.Zoom, Scale: p.scale}
}
func (p *Panel) toPixels(m mathx.Vec2) mathx.Vec2 {
return mathx.Vec2{X: (m.X - p.rect.X) * p.scale, Y: (m.Y - p.rect.Y) * p.scale}
}
// ScreenAt is where a world point was drawn on the last frame, in UI coordinates.
func (p *Panel) ScreenAt(w mathx.Vec2) mathx.Vec2 {
px := p.view.WorldToScreen(w)
return mathx.Vec2{X: p.rect.X + px.X/p.scale, Y: p.rect.Y + px.Y/p.scale}
}
func (p *Panel) worldAt(m mathx.Vec2) mathx.Vec2 {
return p.view.ScreenToWorld(p.toPixels(m))
}
func (p *Panel) gizmoMode(tool uint8) gizmo.Mode {
switch tool {
case context.ToolRotate:
return gizmo.ModeRotate
case context.ToolScale:
return gizmo.ModeScale
}
return gizmo.ModeMove
}
func (p *Panel) gizmoActive() bool {
return p.hasPivot && p.d.Tools.Tool != context.ToolSelect
}
func (p *Panel) snap() gizmo.Snap {
set := *p.d.Tools
return gizmo.Snap{
Enabled: set.Snap != p.ui().Primary(),
Move: set.SnapMove,
Rotate: set.SnapRotate,
Scale: set.SnapScale,
}
}
func (p *Panel) Draw(f *services.Frame, r mathx.Rect) {
p.f = f
ui := f.UI
p.ensure()
p.prune()
p.menuID = ui.ID("ctx")
d := p.d.Docs.Doc()
if d == nil {
p.title = ""
widgets.LabelDim(ui, i18n.Tr("editor.sceneview.no_scene_open", "No scene open"))
return
}
p.updateTitle(d)
p.syncMode(d)
p.updateIsolation(d)
scale := ui.Scale()
pw, ph := int(r.W*scale), int(r.H*scale)
if pw < 8 || ph < 8 {
return
}
target, err := p.pool.Resize(p.target, pw, ph, p.viewMode == Mode3D)
if err != nil {
ui.TextIn(r, err.Error(), ui.Theme.Error, core.AlignCenter)
return
}
p.target = target
p.rect, p.scale = r, scale
if d.View.Camera.Zoom <= 0 {
d.View.Camera.Zoom = 64
}
p.cur = p.worldFor(d)
p.gizmo.Mode = p.gizmoMode(p.d.Tools.Tool)
p.step(d, ui.Time)
p.refreshView(d)
ui.List().Texture(target.Region(), r, mathx.White)
p.agentMarker(d, r)
p.toolbar(d, r)
p.prefabBar(d, r)
p.isolationBar(d, r)
if p.viewMode == Mode3D {
p.c3.SetViewport(float32(target.W), float32(target.H))
p.input3(d, r)
p.drop3(r)
d.View.Orbit = p.c3.Orbit()
} else {
p.updatePivot(d)
p.input(d, r)
p.refreshView(d)
p.updatePivot(d)
p.drop(d, r)
}
p.contextMenu(d)
p.d.Viewport.OnRender(p.hook)
}
func (p *Panel) updatePivot(d *document.SceneDocument) {
p.hasPivot = false
it, ok := p.d.Selection.SelectionState().Active()
if !ok || it.Doc != d.ID {
return
}
s := d.Scene()
h := s.Handle(it.Node)
if h.IsZero() || !s.IsAlive(h) {
return
}
m := s.World(h)
p.pivot = mathx.Vec2{X: m[6], Y: m[7]}
p.rotation = mathx.Atan2(m[1], m[0])
p.activeNode = it.Node
p.hasPivot = true
}
func (p *Panel) hitCtx(ui *core.Context, s *scene.Scene) *gizmo.HitCtx {
return &gizmo.HitCtx{
Space: p.space,
Scene: s,
Node: p.activeNode,
Pivot: p.pivot,
Rotation: p.rotation,
MousePx: p.toPixels(ui.Mouse()),
}
}
func (p *Panel) hitGizmo(d *document.SceneDocument) (gizmo.Gizmo, gizmo.Hit, bool) {
if !p.hasPivot {
return nil, gizmo.Hit{}, false
}
s := d.Scene()
hc := p.hitCtx(p.ui(), s)
if p.gizmos != nil {
var found gizmo.Gizmo
var hit gizmo.Hit
p.gizmos.Each(s, s.Handle(p.activeNode), func(g gizmo.Gizmo) {
if found != nil {
return
}
if h, ok := g.HitTest(hc); ok {
found, hit = g, h
}
})
if found != nil {
return found, hit, true
}
}
if p.gizmoActive() {
if h, ok := p.gizmo.HitTest(hc); ok {
return &p.gizmo, h, true
}
}
return nil, gizmo.Hit{}, false
}
func (p *Panel) input(d *document.SceneDocument, r mathx.Rect) {
ui := p.ui()
id := ui.ID("view")
cam := &d.View.Camera
onBar := p.barRect.Contains(ui.Mouse())
over := !onBar && ui.MouseOver(id, r)
if over && !onBar && p.mode == modeIdle && (ui.MousePressed(input.MouseMiddle) || ui.MousePressed(input.MouseRight)) {
p.mode = modePan
p.panRight = ui.MousePressed(input.MouseRight) && !ui.MousePressed(input.MouseMiddle)
p.panStart = ui.Mouse()
p.panMoved = false
ui.CaptureMouse(id)
}
switch p.mode {
case modePan:
if ui.MouseDown(input.MouseMiddle) || ui.MouseDown(input.MouseRight) {
if ui.Mouse().Sub(p.panStart).Len() > 4 {
p.panMoved = true
}
cam.Position = cam.Position.Sub(ui.MouseDelta().Scale(1 / cam.Zoom))
if p.panMoved {
ui.SetCursorShape(core.CursorMove)
}
} else {
p.mode = modeIdle
ui.ReleaseMouse()
if p.panRight && !p.panMoved {
p.openMenu(d)
}
}
case modeGizmo:
p.dragGizmo(d)
case modeBox:
p.dragBox(d)
}
if over {
if w := ui.ZoomY(); w != 0 {
before := p.worldAt(ui.Mouse())
cam.Zoom = mathx.Clamp(cam.Zoom*float32(math.Pow(1.15, float64(w))), 1, 4096)
p.refreshView(d)
after := p.worldAt(ui.Mouse())
cam.Position = cam.Position.Add(before.Sub(after))
p.refreshView(d)
}
}
if p.mode == modeIdle && !onBar && ui.Pressed(id, r) {
p.press(d, id)
}
p.hot, p.hotGizmo = gizmo.PartNone, nil
if p.mode == modeIdle && over {
if g, h, ok := p.hitGizmo(d); ok {
p.hot, p.hotGizmo = h.Part, g
}
}
}
func (p *Panel) openMenu(d *document.SceneDocument) {
s := d.Scene()
p.menuAt = p.worldAt(p.ui().Mouse())
if h, ok := Pick(s, p.shapes(), p.menuAt); ok {
if id := s.ID(h); !p.d.Selection.IsSelected(id) {
p.d.Selection.Select(id)
}
}
widgets.OpenContextMenu(p.ui(), p.menuID)
}
func (p *Panel) createAt(at mathx.Vec2, anchor *mathx.Vec2) {
if p.d.Templates == nil {
return
}
if p.d.Tools.Snap {
at = snapVec(at, p.d.Tools.SnapMove)
}
p.d.Scenes.Create(ids.NodeID{}, templates.Place{At2: &at}, anchor, nil)
}
func (p *Panel) contextMenu(d *document.SceneDocument) {
ui := p.ui()
if !widgets.ContextMenu(ui, p.menuID) {
return
}
if widgets.MenuItemIcon(ui, i18n.Tr("editor.sceneview.create", "Create…"), p.d.Commands.Shortcut(command.SceneCreate), "ui.plus", true, false) {
anchor := ui.Mouse()
if p.viewMode == Mode3D {
p.createAt3(p.menuAt3, &anchor)
} else {
p.createAt(p.menuAt, &anchor)
}
}
widgets.Separator(ui)
p.item(command.EditDuplicate, i18n.Tr("editor.sceneview.duplicate", "Duplicate"))
p.item(command.EditCopy, i18n.Tr("editor.sceneview.copy", "Copy"))
p.item(command.EditCut, i18n.Tr("editor.sceneview.cut", "Cut"))
p.item(command.EditPaste, i18n.Tr("editor.sceneview.paste", "Paste"))
widgets.Separator(ui)
p.item(command.EditDelete, i18n.Tr("editor.sceneview.delete", "Delete"))
widgets.Separator(ui)
p.item(command.SceneFrame, i18n.Tr("editor.sceneview.frame_selection", "Frame Selection"))
widgets.EndContextMenu(ui)
}
func (p *Panel) toolbar(d *document.SceneDocument, r mathx.Rect) {
ui := p.ui()
th := &ui.Theme
ui.PushClip(r)
defer ui.PopClip()
three := p.viewMode == Mode3D
modeLabel := "2D"
if three {
modeLabel = "3D"
}
zoom := p.ZoomLabel()
h := th.BarHeight
bw := h - 6 + 2
var extra []command.Desc
if !three && p.d.Commands != nil {
extra = p.d.Commands.Toolbar(command.ScopeScene)
}
w := 6 + 8*bw + 18 + ui.TextSize(modeLabel).X + 16 + 6
w += float32(len(extra)) * bw
projLabel := i18n.Tr("editor.sceneview.persp", "Persp")
if p.c3.Cam.Ortho {
projLabel = i18n.Tr("editor.sceneview.ortho", "Ortho")
}
if three {
w += ui.TextSize(projLabel).X + 16
} else {
w += ui.TextSize(zoom).X + 16
}
bar := mathx.Rect{X: r.X + 6, Y: r.Y + 6, W: min(w, r.W-12), H: h}
p.barRect = bar
ui.Shadow(bar, th.Radius.Popup)
ui.RoundRect(bar, th.Radius.Popup, th.BarBg)
tb := widgets.BeginToolbarIn(ui, bar)
tools := [4]struct {
icon string
tip string
}{
{"tool.select", i18n.Tr("editor.sceneview.tool.select", "Select nodes in the scene (Q)")},
{"tool.move", i18n.Tr("editor.sceneview.tool.move", "Move selected nodes (W)")},
{"tool.rotate", i18n.Tr("editor.sceneview.tool.rotate", "Rotate selected nodes (E)")},
{"tool.scale", i18n.Tr("editor.sceneview.tool.scale", "Scale selected nodes (R)")},
}
for i, t := range tools {
if tb.Button(ui.IDInt(300+i), t.icon, t.tip, p.d.Tools.Tool == uint8(i), true) {
p.d.Tools.Tool = uint8(i)
}
}
tb.Separator()
if tb.Button(ui.ID("grid"), "view.grid", i18n.Tr("editor.sceneview.grid", "Show or hide the scene grid"), p.d.Tools.Grid, true) {
p.d.Tools.Grid = !p.d.Tools.Grid
}
if tb.Button(ui.ID("snap"), "view.snap", i18n.Tr("editor.sceneview.snap_hold_ctrl_to_invert", "Snap transforms to steps (hold Ctrl to invert)"), p.d.Tools.Snap, true) {
p.d.Tools.Snap = !p.d.Tools.Snap
}
if tb.Button(ui.ID("vectors"), "view.vectors", i18n.Tr("editor.sceneview.vectors_v", "Show or hide debug vectors (V)"), p.d.Viewport.Overlay.On(vectors.CommandToggle), true) {
p.d.Viewport.Overlay.Toggle(vectors.CommandToggle)
}
for _, cmd := range extra {
checked := cmd.Checked != nil && cmd.Checked()
tip := cmd.TipText()
if tip == "" {
tip = cmd.Title()
}
if tb.Button(ui.ID(cmd.ID), cmd.Icon, tip, checked, p.d.Commands.Enabled(cmd.ID)) {
p.d.Commands.Run(cmd.ID)
}
}
if clicked, _ := tb.TextButton(ui.ID("mode"), modeLabel, i18n.Tr("editor.sceneview.switch_between_the_2d_and", "Switch between the 2D and 3D view")); clicked {
p.toggleMode(d)
}
if three {
if clicked, _ := tb.TextButton(ui.ID("proj"), projLabel, i18n.Tr("editor.sceneview.switch_between_the_perspective_and", "Switch between the perspective and orthographic 3D view")); clicked {
p.ToggleOrtho()
}
}
zid := ui.ID("zoom")
if !three {
if clicked, zr := tb.TextButton(zid, zoom, i18n.Tr("editor.sceneview.zoom", "Choose the scene view zoom")); clicked {
if ui.PopupOpen(zid) {
ui.ClosePopup(zid)
} else {
ui.CloseAllPopups()
ui.OpenPopup(zid, mathx.Vec2{X: zr.X, Y: zr.Y + zr.H})
}
}
}
tb.Separator()
cid := ui.ID("create")
createX := tb.X()
if tb.Button(cid, "ui.plus", i18n.Tr("editor.sceneview.create_button", "Create a node in the scene"), false, true) {
anchor := mathx.Vec2{X: createX, Y: bar.Y + bar.H}
if three {
p.createAt3(p.c3.Cam.Target, &anchor)
} else {
p.createAt(d.View.Camera.Position, &anchor)
}
}
if ui.BeginPopup(zid, 120) {
if widgets.MenuItemIcon(ui, i18n.Tr("editor.sceneview.fit", "Fit"), "", "view.zoom", true, false) {
p.FrameAll()
}
for _, z := range [...]struct {
label string
zoom float32
}{{"50%", 32}, {"100%", 64}, {"200%", 128}, {"400%", 256}} {
if widgets.MenuItem(ui, z.label, "", true, false) {
p.SetZoom(z.zoom)
}
}
ui.EndPopup()
}
}
func (p *Panel) press(d *document.SceneDocument, id core.ID) {
ui := p.ui()
s := d.Scene()
mouse := p.worldAt(ui.Mouse())
if g, h, ok := p.hitGizmo(d); ok {
p.beginGizmo(d, id, g, h, mouse)
return
}
mods := ui.Mods()
if h, ok := Pick(s, p.shapes(), mouse); ok {
nid := s.ID(h)
switch {
case mods.Primary():
p.d.Selection.ToggleSelection(nid)
case mods.Has(input.ModShift):
p.d.Selection.AddToSelection(nid)
default:
if !p.d.Selection.IsSelected(nid) {
p.d.Selection.Select(nid)
}
}
if ui.DoubleClicked(id) && p.d.Tools.Tool == context.ToolSelect {
p.d.Tools.Tool = context.ToolMove
p.gizmo.Mode = gizmo.ModeMove
}
if p.d.Tools.Tool == context.ToolMove && p.d.Selection.IsSelected(nid) && !mods.Primary() {
p.updatePivot(d)
p.beginGizmo(d, id, &p.gizmo, gizmo.Hit{Part: gizmo.PartXY}, mouse)
}
return
}
p.mode = modeBox
p.boxStart, p.boxEnd = ui.Mouse(), ui.Mouse()
p.boxAdd = mods.Primary() || mods.Has(input.ModShift)
ui.CaptureMouse(id)
}
// dragSlop is how far the mouse goes before a press on a node or a handle becomes a drag, so a click never nudges it.
const dragSlop = 4
func gizmoLabel(m gizmo.Mode) string {
switch m {
case gizmo.ModeRotate:
return i18n.Tr("editor.sceneview.rotate", "Rotate")
case gizmo.ModeScale:
return i18n.Tr("editor.sceneview.scale", "Scale")
}
return i18n.Tr("editor.sceneview.move", "Move")
}
func (p *Panel) beginGizmo(d *document.SceneDocument, id core.ID, g gizmo.Gizmo, h gizmo.Hit, mouse mathx.Vec2) {
if reason, locked := d.History.Locked(); locked {
p.d.Notices.Notify(context.LevelWarn, reason)
return
}
s := d.Scene()
tops := clipboard.TopLevel(s, p.d.Selection.Selected())
if len(tops) == 0 {
return
}
nodes := make([]ids.NodeID, len(tops))
for i, t := range tops {
nodes[i] = s.ID(t)
}
label := i18n.Tr("editor.sceneview.edit", "Edit")
if g == gizmo.Gizmo(&p.gizmo) {
label = gizmoLabel(p.gizmo.Mode)
}
p.edit = gizmo.NewEdit(s, nodes, p.activeNode, mouse, p.snap())
p.edit.Tx = d.History.Begin(label)
p.active = g
if g == gizmo.Gizmo(&p.gizmo) && p.d.Dragging != nil {
p.d.Dragging(true)
}
g.Begin(p.edit, h)
p.mode = modeGizmo
p.pressAt, p.moved = p.ui().Mouse(), false
p.ui().CaptureMouse(id)
}
func (p *Panel) dragGizmo(d *document.SceneDocument) {
ui := p.ui()
e, g := p.edit, p.active
finish := func() {
if g == gizmo.Gizmo(&p.gizmo) && p.d.Dragging != nil {
p.d.Dragging(false)
}
p.edit, p.active = nil, nil
p.mode = modeIdle
ui.ReleaseMouse()
}
if e == nil || g == nil {
finish()
return
}
if ui.KeyPressed(input.KeyEscape) {
gizmo.Cancel(e)
e.Tx.Abort()
finish()
return
}
if !p.moved {
if ui.Mouse().Sub(p.pressAt).Len() < dragSlop {
if !ui.MouseDown(input.MouseLeft) {
gizmo.Cancel(e)
e.Tx.Abort()
finish()
}
return
}
p.moved = true
}
if !ui.MouseDown(input.MouseLeft) {
g.End(e)
if e.Err != nil {
e.Tx.Abort()
p.d.Notices.Notify(context.LevelError, e.Err.Error())
} else {
e.Tx.Commit()
}
finish()
return
}
e.Mouse = p.worldAt(ui.Mouse())
e.Snap = p.snap()
g.Update(e)
}
func (p *Panel) dragBox(d *document.SceneDocument) {
ui := p.ui()
p.boxEnd = ui.Mouse()
if ui.MouseDown(input.MouseLeft) {
return
}
p.mode = modeIdle
ui.ReleaseMouse()
if p.boxEnd.Sub(p.boxStart).Len() < 4 {
if !p.boxAdd {
p.d.Selection.Select()
}
return
}
a, b := p.worldAt(p.boxStart), p.worldAt(p.boxEnd)
box := mathx.AABB2{
Min: mathx.Vec2{X: min(a.X, b.X), Y: min(a.Y, b.Y)},
Max: mathx.Vec2{X: max(a.X, b.X), Y: max(a.Y, b.Y)},
}
found := BoxSelect(d.Scene(), p.shapes(), box)
if p.boxAdd {
p.d.Selection.AddToSelection(found...)
} else {
p.d.Selection.Select(found...)
}
}
func (p *Panel) frame(nodes []ids.NodeID) {
d := p.d.Docs.Doc()
if d == nil {
return
}
if pos, zoom, ok := p.frameOf(d, nodes); ok {
d.View.Camera.Position, d.View.Camera.Zoom = pos, zoom
}
}
// frameOf is where the camera looks and how far it zooms to show the nodes, or the whole scene for none.
func (p *Panel) frameOf(d *document.SceneDocument, nodes []ids.NodeID) (mathx.Vec2, float32, bool) {
if p.rect.W <= 0 || p.rect.H <= 0 {
return mathx.Vec2{}, 0, false
}
s := d.Scene()
s.UpdateTransforms()
sh := p.shapes()
if len(nodes) == 0 {
root := s.Root()
for _, h := range s.PreOrder() {
if h != root && s.IsAlive(h) {
nodes = append(nodes, s.ID(h))
}
}
}
var box mathx.AABB2
has := false
for _, id := range nodes {
h := s.Handle(id)
if h.IsZero() || !s.IsAlive(h) {
continue
}
b, _ := Bounds(s, sh, h)
if !has {
box, has = b, true
continue
}
box = box.Union(b)
}
if !has {
return mathx.Vec2{}, 0, false
}
size := box.Size()
size.X, size.Y = max(size.X, 1), max(size.Y, 1)
return box.Center(), mathx.Clamp(min(p.rect.W/(size.X*1.4), p.rect.H/(size.Y*1.4)), 1, 4096), true
}
func snapVec(v mathx.Vec2, step float32) mathx.Vec2 {
if step <= 0 {
return v
}
return mathx.Vec2{
X: float32(math.Round(float64(v.X/step))) * step,
Y: float32(math.Round(float64(v.Y/step))) * step,
}
}
func (p *Panel) drop(d *document.SceneDocument, r mathx.Rect) {
ui := p.ui()
dn := p.f.DnD
if !dn.Hovering(ui, r) {
return
}
pl, _ := dn.Payload()
at := p.worldAt(dn.Pos(ui))
if p.d.Tools.Snap {
at = snapVec(at, p.d.Tools.SnapMove)
}
sp := scene.Space2D
switch pl.Kind {
case context.KindAsset:
ap, ok := pl.Data.(context.AssetPayload)
if !ok || p.d.Templates == nil || !p.d.Templates.Accepts(ap.Kind, &sp) {
dn.Target(ui, r, "reject")
return
}
if got, ok := dn.Target(ui, r, context.KindAsset); ok {
p.spawn(got.Data.(context.AssetPayload), at)
}
case context.KindFiles:
got, ok := dn.Target(ui, r, context.KindFiles)
if !ok || p.d.Pipeline == nil || p.d.Templates == nil {
return
}
files, _ := got.Data.([]string)
created, err := p.d.Files.ImportExternal(files, "assets")
if err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
return
}
for i, id := range created {
if e, ok := p.d.Pipeline.Entry(id); ok && p.d.Templates.Accepts(e.Kind, &sp) {
p.spawn(context.AssetPayload{ID: id, Kind: e.Kind, Name: e.Name}, at.Add(mathx.Vec2{X: float32(i) * 0.5}))
}
}
default:
dn.Target(ui, r, "reject")
}
}
func (p *Panel) spawn(ap context.AssetPayload, at mathx.Vec2) {
sp := scene.Space2D
t, ok := p.d.Templates.ForAsset(ap.Kind, &sp, ap.ID, path.Base(ap.Name))
if !ok {
return
}
if err := p.d.Scenes.InstantiateAt(t, &at); err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
}
}
func (p *Panel) Frame() {
if p.viewMode == Mode3D {
if p.c3 != nil && p.cur != nil {
p.c3.FrameSelection(p.host3())
}
return
}
p.frame(p.d.Selection.Selected())
}
func (p *Panel) FrameAll() {
if p.viewMode == Mode3D {
if p.c3 == nil || p.cur == nil {
return
}
if b, ok := SceneBounds(p.cur.scene, p.shapes()); ok {
p.c3.Cam.Frame(b)
}
return
}
p.frame(nil)
}
// glide moves the camera to a target over a few frames, so following the agent does not jump.
type glide struct {
on bool
doc document.ID
pos mathx.Vec2
zoom float32
last float64
}
// FrameNodes frames the nodes smoothly; in 3D it frames them at once.
func (p *Panel) FrameNodes(nodes []ids.NodeID) {
d := p.d.Docs.Doc()
if d == nil {
return
}
if p.viewMode == Mode3D {
p.frame(nodes)
return
}
pos, zoom, ok := p.frameOf(d, nodes)
if !ok {
return
}
zoom = min(zoom, max(d.View.Camera.Zoom, 16))
p.glide = glide{on: true, doc: d.ID, pos: pos, zoom: zoom}
}
// step moves the camera a part of the way to the glide target each frame.
func (p *Panel) step(d *document.SceneDocument, now float64) {
g := &p.glide
if !g.on || g.doc != d.ID {
g.on = false
return
}
dt := now - g.last
if g.last == 0 || dt <= 0 || dt > 0.1 {
dt = 1.0 / 60
}
g.last = now
k := float32(1 - math.Exp(-dt*9))
cam := &d.View.Camera
cam.Position = cam.Position.Lerp(g.pos, k)
cam.Zoom += (g.zoom - cam.Zoom) * k
if cam.Position.Sub(g.pos).Len()*cam.Zoom < 0.5 && mathx.Abs(cam.Zoom-g.zoom) < 0.01 {
cam.Position, cam.Zoom, g.on = g.pos, g.zoom, false
}
}
// agentMarker draws the agent's avatar above the node it works on, in UI space over the view.
func (p *Panel) agentMarker(d *document.SceneDocument, r mathx.Rect) {
m := p.d.Presence
if m == nil || p.viewMode == Mode3D || p.cur == nil || p.cur.scene == nil {
return
}
sp, at, ok := m.Where(follow.Agent, p.f.Now)
if !ok || sp.Doc != d.Path || sp.Node.IsZero() {
return
}
s := p.cur.scene
h := s.Handle(sp.Node)
if h.IsZero() || !s.IsAlive(h) {
return
}
b, _ := Bounds(s, p.shapes(), h)
top := p.ScreenAt(mathx.Vec2{X: b.Center().X, Y: b.Max.Y})
if bot := p.ScreenAt(mathx.Vec2{X: b.Center().X, Y: b.Min.Y}); bot.Y < top.Y {
top = bot
}
ui := p.ui()
ui.PushClip(r)
wpresence.Marker(ui, mathx.Rect{X: top.X - 9, Y: top.Y - 24, W: 18, H: 18}, p.f.Now-at)
ui.PopClip()
}
// drawAgent outlines the agent's selection and fades the boxes of nodes it removed.
func (p *Panel) drawAgent(s *scene.Scene, dbg *debug.Draw, th *core.Theme) {
doc := p.d.Docs.Doc()
if doc == nil {
return
}
if p.d.AgentSel != nil {
sh := p.shapes()
p.agentNodes = p.d.AgentSel.Nodes(doc.ID, p.agentNodes[:0])
for _, id := range p.agentNodes {
h := s.Handle(id)
if h.IsZero() || !s.IsAlive(h) {
continue
}
if q, ok := Quad(s, sh, h); ok {
pad := p.space.PxToWorld(3)
c := q[0].Add(q[2]).Scale(0.5)
for i := 0; i < 4; i++ {
a := q[i].Add(q[i].Sub(c).Normalized().Scale(pad))
b := q[(i+1)%4].Add(q[(i+1)%4].Sub(c).Normalized().Scale(pad))
dbg.Line(a, b, th.Agent.Presence)
}
}
}
}
if m := p.d.Presence; m != nil {
m.Ghosts(doc.Path, p.f.Now, func(f *presence.Flash, a float32) {
col := th.Agent.DiffRemove
col.A *= a
g := f.Ghost
c := [4]mathx.Vec2{{X: g.X, Y: g.Y}, {X: g.X + g.W, Y: g.Y}, {X: g.X + g.W, Y: g.Y + g.H}, {X: g.X, Y: g.Y + g.H}}
for i := 0; i < 4; i++ {
dbg.Line(c[i], c[(i+1)%4], col)
}
dbg.Line(c[0], c[2], col)
dbg.Line(c[1], c[3], col)
})
}
}
// NodeBounds is a node's world box in the open scene, for follow mode to fade it where it was.
func (p *Panel) NodeBounds(node ids.NodeID) (mathx.Rect, bool) {
d := p.d.Docs.Doc()
if d == nil {
return mathx.Rect{}, false
}
s := d.Scene()
h := s.Handle(node)
if h.IsZero() || !s.IsAlive(h) {
return mathx.Rect{}, false
}
b, _ := Bounds(s, p.shapes(), h)
return mathx.Rect{X: b.Min.X, Y: b.Min.Y, W: b.Max.X - b.Min.X, H: b.Max.Y - b.Min.Y}, true
}
func (p *Panel) drawSelection(s *scene.Scene, dbg *debug.Draw, th *core.Theme) {
sh := p.shapes()
for _, id := range p.d.Selection.Selected() {
h := s.Handle(id)
if h.IsZero() || !s.IsAlive(h) {
continue
}
if q, ok := Quad(s, sh, h); ok {
for i := 0; i < 4; i++ {
dbg.Line(q[i], q[(i+1)%4], th.Accent)
}
continue
}
dbg.Cross(s.WorldPosition(h), p.space.PxToWorld(14), th.Accent)
}
}
func (p *Panel) LastRender() (*rt.Target, uint64) {
d := p.d.Docs.Doc()
if d == nil || d.Scene() != p.capturedScene || p.target == nil || p.capturedTarget != p.target {
return nil, 0
}
return p.target, p.capturedTick
}