Editor/Graph/ParticlePreview.cs
using System;
using Editor;
using Sandbox;
using System.Collections.Generic;
using System.Linq;
namespace fxbox.Graph;
/// <summary>
/// Widget that renders a real-time preview of the particle system using native components
/// </summary>
public class ParticlePreview : SceneRenderingWidget
{
private ParticleResource _resource;
private GameObject _particleSystemObject;
private FXBoxNativeParticleSystem _particleSystem;
private bool _isPlaying = true;
private float _playbackSpeed = 1.0f;
private Vector2 _lastCursorPos;
private Vector2 _angles = new Vector2(45, 30);
private float _distance = 500f;
private float _actualDistance = 500f;
private Vector3 _targetPosition = Vector3.Zero;
private bool _isOrbiting = false;
public float PlaybackSpeed
{
get => _playbackSpeed;
set => _playbackSpeed = value;
}
public ParticlePreview(Widget parent) : base(parent)
{
MouseTracking = true;
FocusMode = FocusMode.Click;
Scene = Scene.CreateEditorScene();
using (Scene.Push())
{
// Setup camera
var cameraGo = new GameObject(true, "camera");
var camera = cameraGo.GetOrAddComponent<CameraComponent>();
camera.BackgroundColor = new Color(0.1f, 0.1f, 0.15f);
camera.ZFar = 10000;
camera.FieldOfView = 60;
Camera = camera;
// Add lighting
var sunGo = new GameObject(true, "sun");
var sun = sunGo.GetOrAddComponent<DirectionalLight>();
sun.WorldRotation = Rotation.FromPitch(50);
sun.LightColor = Color.White;
var ambientGo = new GameObject(true, "ambient");
var ambient = ambientGo.GetOrAddComponent<AmbientLight>();
ambient.Color = Color.Gray * 0.3f;
}
UpdateCameraPosition();
}
public void LoadParticleSystem(ParticleResource resource)
{
_resource = resource;
// Clean up old system
if (_particleSystemObject.IsValid())
{
_particleSystemObject.Destroy();
}
if (_resource != null)
{
using (Scene.Push())
{
// Create new particle system object
_particleSystemObject = new GameObject(true, "ParticleSystem");
_particleSystem = _particleSystemObject.AddComponent<FXBoxNativeParticleSystem>();
_particleSystem.ParticleSystem = _resource;
_particleSystem.PlayOnStart = true;
// Initialize the system
_particleSystem.UpdateEmitters();
}
Log.Info($"Loaded particle system with {_resource.Emitters?.Count ?? 0} emitters");
}
}
public void SetPlaying(bool playing)
{
_isPlaying = playing;
if (_particleSystemObject.IsValid())
{
// Enable/disable all particle effects
foreach (var effect in _particleSystemObject.Children)
{
var particleEffect = effect.GetComponent<ParticleEffect>();
if (particleEffect.IsValid())
{
particleEffect.Enabled = playing;
}
}
}
}
public void TogglePlayback()
{
_isPlaying = !_isPlaying;
SetPlaying(_isPlaying);
Log.Info($"Playback: {(_isPlaying ? "Playing" : "Paused")}");
}
public void Restart()
{
if (_particleSystemObject.IsValid())
{
// Restart by rebuilding the entire system
LoadParticleSystem(_resource);
Log.Info("Particle system restarted");
}
}
protected override void PreFrame()
{
Scene.EditorTick(RealTime.Now, RealTime.Delta);
DrawGizmos();
// Force continuous updates
Update();
}
private void DrawGizmos()
{
if (_resource == null) return;
// Draw spawn shape for first emitter
if (_resource.Emitters.Count > 0)
{
var emitter = _resource.Emitters[0];
var posModule = emitter.InitializeModules.OfType<InitializePositionModule>().FirstOrDefault();
if (posModule != null)
{
Gizmo.Draw.Color = Color.Yellow.WithAlpha(0.3f);
Gizmo.Draw.LineThickness = 2;
switch (posModule.Shape)
{
case InitializePositionModule.SpawnShape.Sphere:
Gizmo.Draw.LineSphere(new Sphere(Vector3.Zero, posModule.Radius));
break;
case InitializePositionModule.SpawnShape.Box:
Gizmo.Draw.LineBBox(BBox.FromPositionAndSize(Vector3.Zero, posModule.BoxSize));
break;
case InitializePositionModule.SpawnShape.Cone:
DrawConeGizmo(posModule);
break;
case InitializePositionModule.SpawnShape.Circle:
Gizmo.Draw.LineCircle(Vector3.Zero, Vector3.Forward, posModule.Radius);
break;
case InitializePositionModule.SpawnShape.Line:
Gizmo.Draw.Line(posModule.LineStart, posModule.LineEnd);
break;
}
}
}
DrawGrid();
}
private void DrawConeGizmo(InitializePositionModule module)
{
var height = module.Radius;
var radius = MathF.Tan(module.ConeAngle.DegreeToRadian()) * height;
// Draw cone base
Gizmo.Draw.LineCircle(Vector3.Forward * height, Vector3.Forward, radius);
// Draw cone lines
var points = 8;
for (int i = 0; i < points; i++)
{
var angle = (i / (float)points) * 360f;
var dir = new Vector3(
MathF.Cos(angle.DegreeToRadian()) * radius,
MathF.Sin(angle.DegreeToRadian()) * radius,
height
);
Gizmo.Draw.Line(Vector3.Zero, dir);
}
}
private void DrawGrid()
{
if (_resource?.PreviewSettings?.ShowGrid ?? true)
{
Gizmo.Draw.Color = Color.White.WithAlpha(0.1f);
Gizmo.Draw.LineThickness = 1;
// Draw XY grid
for (int x = -500; x <= 500; x += 100)
{
Gizmo.Draw.Line(new Vector3(x, -500, 0), new Vector3(x, 500, 0));
}
for (int y = -500; y <= 500; y += 100)
{
Gizmo.Draw.Line(new Vector3(-500, y, 0), new Vector3(500, y, 0));
}
}
if (_resource?.PreviewSettings?.ShowGround ?? true)
{
Gizmo.Draw.Color = Color.Gray.WithAlpha(0.2f);
Gizmo.Draw.LineBBox(new BBox(new Vector3(-500, -500, -1), new Vector3(500, 500, 0)));
}
UpdateCameraPosition();
}
private void UpdateCameraPosition()
{
if (!Camera.IsValid()) return;
Camera.WorldRotation = new Angles(_angles.y, -_angles.x, 0);
_actualDistance = _actualDistance.LerpTo( _distance, Time.Delta * 15f );
Camera.WorldPosition = _targetPosition + Camera.WorldRotation.Backward * _actualDistance;
}
protected override void OnMousePress(MouseEvent e)
{
base.OnMousePress(e);
if (e.LeftMouseButton)
{
_isOrbiting = true;
_lastCursorPos = e.ScreenPosition;
}
}
protected override void OnMouseReleased(MouseEvent e)
{
base.OnMouseReleased(e);
if (e.LeftMouseButton)
{
_isOrbiting = false;
}
}
protected override void OnMouseMove(MouseEvent e)
{
base.OnMouseMove(e);
if (_isOrbiting)
{
var delta = e.ScreenPosition - _lastCursorPos;
_angles.x += delta.x * 0.3f;
_angles.y += delta.y * 0.3f;
_angles.y = _angles.y.Clamp(-90, 90);
//UpdateCameraPosition();
_lastCursorPos = e.ScreenPosition;
}
}
protected override void OnMouseWheel( WheelEvent e )
{
base.OnMouseWheel(e);
_distance -= e.Delta * 1f;
_distance = _distance.Clamp(50, 5000);
}
protected override void OnPaint()
{
base.OnPaint();
// Draw overlay info
Paint.SetPen(Theme.Text);
Paint.SetDefaultFont();
// Count particles from all emitter objects
int totalParticles = 0;
int totalMax = _resource?.Emitters.Sum(e => e.MaxParticles) ?? 0;
if (_particleSystemObject.IsValid())
{
foreach (var child in _particleSystemObject.Children)
{
var effect = child.GetComponent<ParticleEffect>();
if (effect.IsValid())
{
totalParticles += effect.Particles.Count;
}
}
}
var text = $"Particles: {totalParticles} / {totalMax}";
Paint.DrawText(new Rect(10, 10, 200, 30), text, TextFlag.LeftTop);
var stateText = _isPlaying ? "PLAYING" : "PAUSED";
Paint.DrawText(new Rect(10, 40, 200, 30), stateText, TextFlag.LeftTop);
// Draw emitter count
var emitterText = $"Emitters: {_resource?.Emitters.Count ?? 0}";
Paint.DrawText(new Rect(10, 70, 200, 30), emitterText, TextFlag.LeftTop);
}
}