298 lines
8.5 KiB
Go
298 lines
8.5 KiB
Go
package editor
|
|
|
|
import (
|
|
"aego/asset"
|
|
"aego/core/ids"
|
|
"aego/core/mathx"
|
|
"aego/editor/core/context"
|
|
"aego/editor/core/document"
|
|
"aego/editor/core/preview"
|
|
"aego/editor/core/workspace"
|
|
"aego/editor/ext"
|
|
"aego/engine/i18n"
|
|
"aego/modules/particles"
|
|
"aego/scene"
|
|
"aego/scene/frame"
|
|
"aego/ui/core"
|
|
"aego/ui/widgets"
|
|
"fmt"
|
|
)
|
|
|
|
type transport struct {
|
|
sim *particles.Preview
|
|
cfg particles.ParticleSystem2D
|
|
speed float32
|
|
seed uint64
|
|
locked, paused bool
|
|
showSettings bool
|
|
bad string
|
|
}
|
|
|
|
func resolver(ed *context.Editor) asset.Resolver {
|
|
if ed.Loader == nil {
|
|
return nil
|
|
}
|
|
return ed.Loader
|
|
}
|
|
func (v *transport) init(ed *context.Editor) {
|
|
if v.sim == nil {
|
|
v.sim = particles.NewPreview(ed.Types, resolver(ed))
|
|
v.speed = 1
|
|
v.seed = 42
|
|
v.locked = true
|
|
}
|
|
}
|
|
func (v *transport) configure(ed *context.Editor, c particles.ParticleSystem2D, first bool) {
|
|
v.init(ed)
|
|
if err := v.sim.Configure(c, v.seed); err != nil {
|
|
v.bad = err.Error()
|
|
return
|
|
}
|
|
first = first || v.cfg.MaxParticles == 0
|
|
v.bad = ""
|
|
v.cfg = c
|
|
if first && !c.PlayOnStart {
|
|
v.restart()
|
|
}
|
|
}
|
|
func (v *transport) restart() {
|
|
if v.sim == nil {
|
|
return
|
|
}
|
|
v.sim.Restart()
|
|
if !v.cfg.PlayOnStart && v.cfg.Rate == 0 && len(v.cfg.Bursts) == 0 {
|
|
v.sim.Burst(8)
|
|
}
|
|
v.paused = false
|
|
}
|
|
func (v *transport) close() {
|
|
if v.sim != nil {
|
|
v.sim.Close()
|
|
v.sim = nil
|
|
}
|
|
}
|
|
func (v *transport) buttons(ed *context.Editor) {
|
|
ui := ed.UI
|
|
if ui == nil || v.sim == nil {
|
|
return
|
|
}
|
|
if v.bad != "" {
|
|
widgets.LabelWrapped(ui, v.bad, true)
|
|
}
|
|
play := v.paused || !v.sim.Playing()
|
|
toggle, toggleTip := i18n.Tr("editor.particles.pause_short", "Pause"), i18n.Tr("editor.particles.pause", "Pause preview")
|
|
if play {
|
|
toggle, toggleTip = i18n.Tr("editor.common.play", "Play"), i18n.Tr("editor.particles.play", "Play preview")
|
|
}
|
|
labels := [4]string{toggle, i18n.Tr("editor.particles.restart_short", "Restart"), i18n.Tr("editor.particles.burst", "Burst (8)"), i18n.Tr("editor.particles.clear_short", "Clear")}
|
|
tips := [4]string{toggleTip, i18n.Tr("editor.particles.restart", "Restart preview"), labels[2], i18n.Tr("editor.particles.clear", "Clear particles")}
|
|
var need float32
|
|
for _, label := range labels {
|
|
need = max(need, ui.TextSize(label).X+2*ui.Theme.Padding+4)
|
|
}
|
|
const gap = float32(4)
|
|
cols := transportColumns(ui.Cursor().W, need, gap)
|
|
var row mathx.Rect
|
|
for i, label := range labels {
|
|
if i%cols == 0 {
|
|
row = ui.Cursor().Next(0)
|
|
}
|
|
w := (row.W - gap*float32(cols-1)) / float32(cols)
|
|
rect := mathx.Rect{X: row.X + float32(i%cols)*(w+gap), Y: row.Y, W: w, H: row.H}
|
|
id := core.IDSeq(ui.ID("particle.transport"), i+1)
|
|
hit := widgets.ButtonRect(ui, id, rect, label)
|
|
widgets.Tooltip(ui, id, tips[i])
|
|
if !hit {
|
|
continue
|
|
}
|
|
switch i {
|
|
case 0:
|
|
if play && !v.paused {
|
|
v.sim.Play()
|
|
}
|
|
v.paused = !play
|
|
case 1:
|
|
if !v.locked {
|
|
v.seed++
|
|
_ = v.sim.Configure(v.cfg, v.seed)
|
|
}
|
|
v.restart()
|
|
case 2:
|
|
v.sim.Burst(8)
|
|
case 3:
|
|
v.sim.Clear()
|
|
}
|
|
}
|
|
widgets.LabelDim(ui, fmt.Sprintf("%s: %d", i18n.Tr("editor.particles.alive", "Alive"), v.sim.Count()))
|
|
if widgets.TreeNode(ui, ui.ID("fx.preview.options"), i18n.Tr("editor.particles.preview_options", "Preview settings"), &v.showSettings, false) {
|
|
v.showSettings = !v.showSettings
|
|
}
|
|
if v.showSettings {
|
|
widgets.Checkbox(ui, i18n.Tr("editor.particles.lock_seed", "Lock preview seed"), &v.locked)
|
|
widgets.DragFloat(ui, ui.ID("fx.speed"), i18n.Tr("editor.particles.speed", "Preview speed"), &v.speed, .02)
|
|
v.speed = mathx.Clamp(v.speed, 0, 4)
|
|
}
|
|
}
|
|
|
|
func transportColumns(width, buttonWidth, gap float32) int {
|
|
if width >= 4*buttonWidth+3*gap {
|
|
return 4
|
|
}
|
|
if width >= 2*buttonWidth+gap {
|
|
return 2
|
|
}
|
|
return 1
|
|
}
|
|
|
|
type adapter struct {
|
|
panels map[document.ID]*presetPanel
|
|
ed *context.Editor
|
|
reg *ext.Registry
|
|
source *scene.Scene
|
|
id ids.NodeID
|
|
rev uint64
|
|
gen uint32
|
|
last float64
|
|
sel transport
|
|
}
|
|
|
|
func Hook(r *ext.Registry) error {
|
|
ed := r.Editor()
|
|
if ed == nil {
|
|
return nil
|
|
}
|
|
p := &adapter{ed: ed, reg: r, panels: map[document.ID]*presetPanel{}}
|
|
if err := registerInspector(r, p); err != nil {
|
|
return err
|
|
}
|
|
if ed.Docs == nil {
|
|
return nil
|
|
}
|
|
unsub := ed.Docs.Subscribe(func(evt document.Event) {
|
|
if evt.Kind == document.EvClosed {
|
|
if x := p.panels[evt.Doc]; x != nil && ed.Docs.AssetDocument(x.d.Asset) == nil {
|
|
x.dispose()
|
|
p.reg.ClosePanel(x.ID())
|
|
}
|
|
}
|
|
})
|
|
if ed.Engine != nil {
|
|
ed.Engine.OnClose(func() {
|
|
unsub()
|
|
p.sel.close()
|
|
for _, x := range p.panels {
|
|
x.dispose()
|
|
}
|
|
})
|
|
}
|
|
ed.ScenePreviews = append(ed.ScenePreviews, func(runner *frame.Runner) {
|
|
if _, off := r.Disabled()[particles.ID]; off {
|
|
return
|
|
}
|
|
if w := ed.Docs.ActiveWorld(); w != nil {
|
|
if live := particles.Of(w); live != nil {
|
|
live.AppendToView(runner)
|
|
}
|
|
return
|
|
}
|
|
if runner.Scene() != p.source || p.source == nil || p.sel.sim == nil {
|
|
return
|
|
}
|
|
h := p.source.Handle(p.id)
|
|
if !p.source.IsAlive(h) || runner.Hidden(h) {
|
|
return
|
|
}
|
|
pose := p.source.RenderWorld(h)
|
|
if p.source.EffectiveEnabled(h) && !p.source.Under3D(h) {
|
|
p.sel.sim.ExtractLayers(runner, pose, p.source.Layers(h))
|
|
}
|
|
})
|
|
r.Tick(p.tick)
|
|
r.Profiler(i18n.T("editor.particles.profiler", "Particles"), true, func(ed *context.Editor) {
|
|
if ed.Play == nil || ed.Play.World() == nil {
|
|
return
|
|
}
|
|
s := particles.Of(ed.Play.World())
|
|
if s == nil {
|
|
return
|
|
}
|
|
st := s.Stats()
|
|
widgets.LabelDim(ed.UI, fmt.Sprintf("%s: %d %d / %d", i18n.Tr("editor.particles.emitters", "Emitters"), st.Emitters, st.Alive, st.Capacity))
|
|
widgets.LabelDim(ed.UI, fmt.Sprintf("%s: %d / %d", i18n.Tr("editor.particles.spawned_dropped", "Born / dropped this frame"), st.BornThisFrame, st.DroppedThisFrame))
|
|
widgets.LabelDim(ed.UI, fmt.Sprintf("CPU ms: %.3f / %.3f / %.3f", float64(st.SimulateNS)/1e6, float64(st.ExtractNS)/1e6, float64(st.SortNS)/1e6))
|
|
widgets.LabelDim(ed.UI, fmt.Sprintf("%s: %d / %d", i18n.Tr("editor.particles.culling", "Visible / culled emitters"), st.VisibleEmitters, st.CulledEmitters))
|
|
widgets.LabelDim(ed.UI, fmt.Sprintf("%s: %d / %d B / %d", i18n.Tr("editor.particles.uploads", "Uploads / bytes / draws"), st.InstanceUploads, st.UploadBytes, st.Draws))
|
|
})
|
|
open := r.DocumentPanelFactory(func(d *document.AssetDocument) workspace.Panel { return p.panel(d) })
|
|
if err := r.DocumentHandler(document.Handler{ID: "particles.preset", Name: i18n.T("editor.particles.preset", "Particle preset"), Icon: "file.preset", Kinds: []asset.ArtifactKind{asset.ArtPreset}, Roles: []string{particles.NameSystem}, TextSource: true, Capabilities: document.View | document.EditSource | document.Preview | document.RevealSource, Open: open}); err != nil {
|
|
return err
|
|
}
|
|
if err := r.CreateAction(context.CreateAction{
|
|
Key: "particles.create-preset", Name: i18n.Tr("editor.particles.create_preset_file", "Particle Preset (.aepreset)…"),
|
|
Category: i18n.Tr("editor.assets.create_category", "Files"), Icon: "file.preset",
|
|
Run: func(ids.NodeID) error { return askCreatePreset(ed, ed.AssetCreateDirectory("assets")) },
|
|
RunInFolder: func(dir string) error { return askCreatePreset(ed, dir) },
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := p.registerEffectCreate(); err != nil {
|
|
return err
|
|
}
|
|
return r.Thumbnail(asset.ArtPreset, preview.Handler{Component: particles.NameSystem, ReadSource: true, Version: "particles.v2", Render: p.thumbnail, Build: p.card})
|
|
}
|
|
func (p *adapter) tick() {
|
|
ed := p.ed
|
|
sc := ed.Scene()
|
|
var id ids.NodeID
|
|
if sc != nil && !ed.Doc().IsPlay() {
|
|
if sel := ed.Selected(); len(sel) == 1 {
|
|
id = sel[0]
|
|
}
|
|
}
|
|
var c *particles.ParticleSystem2D
|
|
if sc != nil && !id.IsZero() {
|
|
h := sc.Handle(id)
|
|
if sc.IsAlive(h) {
|
|
sc.RenderWorld(h)
|
|
if sc.EffectiveEnabled(h) && !sc.Under3D(h) {
|
|
c, _ = scene.Get[particles.ParticleSystem2D](sc, h)
|
|
}
|
|
}
|
|
}
|
|
if c == nil {
|
|
p.sel.close()
|
|
p.source = nil
|
|
p.last = 0
|
|
return
|
|
}
|
|
now := ed.Now()
|
|
first := p.source != sc || p.id != id
|
|
if first {
|
|
p.sel.close()
|
|
p.sel = transport{}
|
|
p.source = sc
|
|
p.id = id
|
|
p.last = now
|
|
}
|
|
var gen uint32
|
|
if ed.Loader != nil {
|
|
gen = ed.Loader.Generation()
|
|
}
|
|
if first || sc.Revision() != p.rev || gen != p.gen {
|
|
p.sel.init(ed)
|
|
p.sel.sim.Step(0, sc.RenderWorld(sc.Handle(id)))
|
|
p.sel.configure(ed, *c, first)
|
|
p.rev = sc.Revision()
|
|
p.gen = gen
|
|
}
|
|
if !p.sel.paused && p.sel.sim != nil {
|
|
p.sel.sim.Step(max(0, min(now-p.last, .1))*float64(p.sel.speed), sc.RenderWorld(sc.Handle(id)))
|
|
}
|
|
p.last = now
|
|
}
|
|
func (p *adapter) controls(ed *context.Editor, id ids.NodeID) {
|
|
if p.id == id && p.sel.sim != nil {
|
|
p.sel.buttons(ed)
|
|
}
|
|
}
|