Files
sanspie 5eb1832907
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 15m28s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 7m47s
Complete Stage 12 for v0.12.0
2026-11-05 11:46:53 +03:00

240 lines
5.8 KiB
Go

package editor
import (
"aego/asset"
"aego/core/mathx"
"aego/editor/core/context"
"aego/editor/core/preview"
"aego/format/aetext"
"aego/gpu"
"aego/modules/particles"
"aego/render/rt"
"aego/render/view"
"aego/scene/frame"
"errors"
"fmt"
"math"
"strings"
)
type surface struct {
pool *rt.Pool
target *rt.Target
runner *frame.Runner
generation int
}
func (s *surface) close() {
if s.pool != nil {
if s.target != nil {
s.pool.Release(s.target)
}
s.pool.Close()
}
if s.runner != nil {
s.runner.Close()
}
*s = surface{}
}
func (s *surface) prepare(ed *context.Editor, size int) error {
if ed.Renderer == nil || strings.Contains(strings.ToLower(ed.Renderer.Device().Caps().Renderer), "null") {
return errors.New("GPU preview unavailable")
}
dev := ed.Renderer.Device()
g := 0
if d, ok := dev.(interface {
Lost() bool
Restores() int
}); ok {
if d.Lost() {
return errors.New("graphics context lost")
}
g = d.Restores()
}
if s.generation != g {
s.close()
}
if s.target != nil {
return nil
}
s.generation = g
s.pool = rt.New(dev, gpu.TexSRGBA8, "editor.particles")
var err error
s.target, err = s.pool.Acquire(size, size, false)
if err != nil {
s.close()
return err
}
s.runner = frame.New()
s.runner.Bind(nil, ed.Renderer, resolver(ed))
return nil
}
func (s *surface) render(v *transport, fixed bool) {
t := s.target
pose := mathx.Mat3Identity()
extent := float32(2)
center := mathx.V2(0, 0)
if b, ok := v.sim.Bounds(pose); ok {
sz := b.Size()
if fixed {
extent = max(.25, sz.X, sz.Y) * 1.22
center = b.Center()
} else {
extent = max(2, sz.X, sz.Y) * 1.3
center = b.Center().Scale(.7)
}
}
size := float32(t.W)
out := view.View{Target: t.Texture, TargetW: t.BucketW, TargetH: t.BucketH, Viewport: mathx.Rect{W: size, H: size}, PixelsPerUnit: 1, PixelScale: 1, Layers: view.AllLayers, Camera: view.Camera2D{Zoom: size / extent, Position: center}, ZMax: 1, Clear: mathx.Color{R: .075, G: .085, B: .10, A: 1}, DoClear: true, Label: "particles.preview"}
s.runner.Prepare(t.W, t.H, 1)
s.runner.AddView(out, 0)
v.sim.Extract(s.runner, pose)
s.runner.Submit(s.runner.Renderer())
}
func (p *adapter) presetSource(e asset.Entry) ([]byte, error) {
if d := p.ed.Docs.Script(e.Source); d != nil {
return d.Code.Bytes(), nil
}
a, ok := e.Artifact(asset.ArtPreset)
if !ok {
return nil, errors.New("preset artifact missing")
}
return p.ed.Pipeline.Artifacts().ReadFile(a.Path)
}
func particleNode(data []byte, name string) (*aetext.Node, *aetext.Node, error) {
n, err := aetext.Parse(data, name)
if err != nil {
return nil, nil, err
}
for _, node := range n.Blocks {
if node.Head != "node" {
continue
}
for _, c := range node.Blocks {
if c.Head == "component" && c.Label == particles.NameSystem {
return n, c, nil
}
}
}
return nil, nil, preview.ErrUnsupportedContent
}
func (p *adapter) thumbnail(e asset.Entry, size int) (preview.Surface, error) {
data, err := p.presetSource(e)
if err != nil {
return preview.Surface{}, err
}
if _, _, err = particleNode(data, e.Source); err != nil {
return preview.Surface{}, err
}
c, err := particles.ReadPreset(p.ed.Types, resolver(p.ed), data)
if err != nil {
return preview.Surface{}, err
}
var v transport
defer v.close()
v.configure(p.ed, c, true)
if v.bad != "" {
return preview.Surface{}, errors.New(v.bad)
}
v.sampleThumbnail()
var s surface
if err = s.prepare(p.ed, size); err != nil {
return preview.Surface{}, err
}
s.render(&v, true)
return preview.Surface{Target: s.target.Texture, Close: s.close}, nil
}
func (p *adapter) card(card *preview.Card) (bool, error) {
_, node, err := particleNode(card.Source, card.Entry.Source)
if errors.Is(err, preview.ErrUnsupportedContent) {
return false, nil
}
if err != nil {
return true, err
}
card.Label(4, 3, "PARTICLES", true)
d := particles.DefaultParticleSystem2D()
shape, rate, capacity := "point", d.Rate, d.MaxParticles
if a, ok := node.Field("shape"); ok && a.Ident != "" {
shape = a.Ident
}
if a, ok := node.Field("rate"); ok {
rate = float32(a.AsFloat())
}
if a, ok := node.Field("maxParticles"); ok {
capacity = int32(a.AsInt())
}
card.Label(4, 19, fmt.Sprintf("%s %.1f/s", shape, rate), false)
card.Label(4, 33, fmt.Sprintf("Pool: %d", capacity), false)
if value, ok := node.Field("colorOverLife"); ok {
card.Label(4, 47, fmt.Sprintf("Gradient: %d keys", len(value.List)), false)
}
card.Label(4, 63, "GPU unavailable", false)
return true, nil
}
func (v *transport) sampleThumbnail() {
c := &v.cfg
pose := mathx.Mat3Identity()
steps := min(24, max(1, int(c.Lifetime.X*18)))
if c.Loop && c.Rate > 0 {
steps = min(150, max(60, int(c.Lifetime.Y*75)))
}
work := 0
for range steps {
v.sim.Step(1./60, pose)
work += max(1, v.sim.Count())
if work >= 131072 {
break
}
}
if v.sim.Count() != 0 {
return
}
// A short burst may be gone by the standard sample, or may be scheduled
// much later. Fast-forward the same schedule to its next birth.
v.restart()
if v.sim.Count() != 0 {
return
}
warm := float64(0)
if c.Loop {
warm = float64(c.Prewarm)
}
next := math.Inf(1)
duration := float64(c.Duration)
if c.Rate > 0 {
rate := float64(c.Rate)
next = (1 - math.Mod(warm*rate, 1)) / rate
if !c.Loop && next > duration {
next = math.Inf(1)
}
}
local := math.Mod(warm, duration)
for _, key := range c.Bursts {
if key.Y <= 0 {
continue
}
delay := float64(key.X) - local
if delay < 0 {
if !c.Loop {
continue
}
delay += duration
}
next = min(next, delay)
}
if math.IsInf(next, 1) {
return
}
// Splitting just before the birth keeps the bounded catch-up policy from
// dropping a very sparse event along with whole empty cycles.
before := math.Nextafter(next, 0)
if before > 0 {
v.sim.Step(before, pose)
}
v.sim.Step(next-before+min(1./60, float64(c.Lifetime.X)*.25), pose)
}