Skip to main content

proof_engine/editor/
vfx_graph.rs

1
2//! VFX Graph editor — node-based visual effects graph with GPU particle simulation,
3//! event handling, spawn contexts, attribute system, and full block library.
4
5use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
6use std::collections::HashMap;
7
8// ---------------------------------------------------------------------------
9// Attribute types
10// ---------------------------------------------------------------------------
11
12#[derive(Debug, Clone, PartialEq)]
13pub enum VfxAttributeType {
14    Float,
15    Float2,
16    Float3,
17    Float4,
18    Int,
19    Uint,
20    Bool,
21    Color,
22    Matrix4x4,
23    Mesh,
24    Texture2D,
25    Texture3D,
26    TextureCube,
27    AnimationCurve,
28    Gradient,
29    SdfVolume,
30    PointCache,
31}
32
33impl VfxAttributeType {
34    pub fn label(&self) -> &'static str {
35        match self {
36            Self::Float => "Float",
37            Self::Float2 => "Vector2",
38            Self::Float3 => "Vector3",
39            Self::Float4 => "Vector4",
40            Self::Int => "Int",
41            Self::Uint => "UInt",
42            Self::Bool => "Bool",
43            Self::Color => "Color",
44            Self::Matrix4x4 => "Matrix4x4",
45            Self::Mesh => "Mesh",
46            Self::Texture2D => "Texture2D",
47            Self::Texture3D => "Texture3D",
48            Self::TextureCube => "TextureCube",
49            Self::AnimationCurve => "AnimationCurve",
50            Self::Gradient => "Gradient",
51            Self::SdfVolume => "SDF Volume",
52            Self::PointCache => "Point Cache",
53        }
54    }
55
56    pub fn port_color(&self) -> Vec4 {
57        match self {
58            Self::Float => Vec4::new(0.55, 0.8, 0.55, 1.0),
59            Self::Float2 => Vec4::new(0.55, 0.72, 0.87, 1.0),
60            Self::Float3 => Vec4::new(0.55, 0.55, 0.87, 1.0),
61            Self::Float4 => Vec4::new(0.72, 0.55, 0.87, 1.0),
62            Self::Int => Vec4::new(0.5, 0.8, 0.5, 1.0),
63            Self::Uint => Vec4::new(0.4, 0.75, 0.4, 1.0),
64            Self::Bool => Vec4::new(0.87, 0.55, 0.55, 1.0),
65            Self::Color => Vec4::new(0.87, 0.87, 0.25, 1.0),
66            Self::Matrix4x4 => Vec4::new(0.75, 0.65, 0.45, 1.0),
67            Self::Mesh => Vec4::new(0.87, 0.6, 0.35, 1.0),
68            Self::Texture2D | Self::Texture3D | Self::TextureCube => Vec4::new(0.65, 0.45, 0.75, 1.0),
69            Self::AnimationCurve => Vec4::new(0.35, 0.65, 0.65, 1.0),
70            Self::Gradient => Vec4::new(0.85, 0.65, 0.35, 1.0),
71            Self::SdfVolume => Vec4::new(0.35, 0.85, 0.65, 1.0),
72            Self::PointCache => Vec4::new(0.65, 0.85, 0.35, 1.0),
73        }
74    }
75
76    pub fn default_value(&self) -> VfxValue {
77        match self {
78            Self::Float => VfxValue::Float(0.0),
79            Self::Float2 => VfxValue::Float2(Vec2::ZERO),
80            Self::Float3 => VfxValue::Float3(Vec3::ZERO),
81            Self::Float4 => VfxValue::Float4(Vec4::ZERO),
82            Self::Int => VfxValue::Int(0),
83            Self::Uint => VfxValue::Uint(0),
84            Self::Bool => VfxValue::Bool(false),
85            Self::Color => VfxValue::Color(Vec4::ONE),
86            Self::Matrix4x4 => VfxValue::Matrix4x4(Mat4::IDENTITY),
87            _ => VfxValue::None,
88        }
89    }
90}
91
92// ---------------------------------------------------------------------------
93// VFX Values
94// ---------------------------------------------------------------------------
95
96#[derive(Debug, Clone)]
97pub enum VfxValue {
98    None,
99    Float(f32),
100    Float2(Vec2),
101    Float3(Vec3),
102    Float4(Vec4),
103    Int(i32),
104    Uint(u32),
105    Bool(bool),
106    Color(Vec4),
107    Matrix4x4(Mat4),
108}
109
110impl VfxValue {
111    pub fn as_float(&self) -> f32 {
112        match self {
113            Self::Float(v) => *v,
114            Self::Int(v) => *v as f32,
115            Self::Uint(v) => *v as f32,
116            Self::Bool(v) => if *v { 1.0 } else { 0.0 },
117            _ => 0.0,
118        }
119    }
120
121    pub fn as_float3(&self) -> Vec3 {
122        match self {
123            Self::Float3(v) => *v,
124            Self::Float4(v) => Vec3::new(v.x, v.y, v.z),
125            Self::Color(v) => Vec3::new(v.x, v.y, v.z),
126            Self::Float(v) => Vec3::splat(*v),
127            _ => Vec3::ZERO,
128        }
129    }
130
131    pub fn as_color(&self) -> Vec4 {
132        match self {
133            Self::Color(v) => *v,
134            Self::Float4(v) => *v,
135            Self::Float3(v) => Vec4::new(v.x, v.y, v.z, 1.0),
136            _ => Vec4::ONE,
137        }
138    }
139
140    pub fn type_of(&self) -> VfxAttributeType {
141        match self {
142            Self::None => VfxAttributeType::Bool,
143            Self::Float(_) => VfxAttributeType::Float,
144            Self::Float2(_) => VfxAttributeType::Float2,
145            Self::Float3(_) => VfxAttributeType::Float3,
146            Self::Float4(_) => VfxAttributeType::Float4,
147            Self::Int(_) => VfxAttributeType::Int,
148            Self::Uint(_) => VfxAttributeType::Uint,
149            Self::Bool(_) => VfxAttributeType::Bool,
150            Self::Color(_) => VfxAttributeType::Color,
151            Self::Matrix4x4(_) => VfxAttributeType::Matrix4x4,
152        }
153    }
154}
155
156// ---------------------------------------------------------------------------
157// Built-in particle attributes
158// ---------------------------------------------------------------------------
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
161pub enum BuiltinAttribute {
162    Position,
163    Velocity,
164    OldPosition,
165    Direction,
166    Size,
167    SizeX,
168    SizeY,
169    SizeZ,
170    Scale,
171    Color,
172    Alpha,
173    AliveTime,
174    TotalLifetime,
175    Age,
176    AgeNormalized,
177    Mass,
178    AngleX,
179    AngleY,
180    AngleZ,
181    AngularVelocityX,
182    AngularVelocityY,
183    AngularVelocityZ,
184    TexIndex,
185    PivotX,
186    PivotY,
187    PivotZ,
188    Random,
189    ParticleId,
190    SpawnIndex,
191    StripIndex,
192}
193
194impl BuiltinAttribute {
195    pub fn label(&self) -> &'static str {
196        match self {
197            Self::Position => "position",
198            Self::Velocity => "velocity",
199            Self::OldPosition => "oldPosition",
200            Self::Direction => "direction",
201            Self::Size => "size",
202            Self::SizeX => "sizeX",
203            Self::SizeY => "sizeY",
204            Self::SizeZ => "sizeZ",
205            Self::Scale => "scale",
206            Self::Color => "color",
207            Self::Alpha => "alpha",
208            Self::AliveTime => "alive",
209            Self::TotalLifetime => "lifetime",
210            Self::Age => "age",
211            Self::AgeNormalized => "ageNormalized",
212            Self::Mass => "mass",
213            Self::AngleX => "angleX",
214            Self::AngleY => "angleY",
215            Self::AngleZ => "angleZ",
216            Self::AngularVelocityX => "angularVelocityX",
217            Self::AngularVelocityY => "angularVelocityY",
218            Self::AngularVelocityZ => "angularVelocityZ",
219            Self::TexIndex => "texIndex",
220            Self::PivotX => "pivotX",
221            Self::PivotY => "pivotY",
222            Self::PivotZ => "pivotZ",
223            Self::Random => "random",
224            Self::ParticleId => "particleId",
225            Self::SpawnIndex => "spawnIndex",
226            Self::StripIndex => "stripIndex",
227        }
228    }
229
230    pub fn attribute_type(&self) -> VfxAttributeType {
231        match self {
232            Self::Position | Self::Velocity | Self::OldPosition | Self::Direction => VfxAttributeType::Float3,
233            Self::Color => VfxAttributeType::Color,
234            Self::Alpha | Self::Size | Self::SizeX | Self::SizeY | Self::SizeZ | Self::Scale => VfxAttributeType::Float,
235            Self::AliveTime | Self::TotalLifetime | Self::Age | Self::AgeNormalized | Self::Mass => VfxAttributeType::Float,
236            Self::AngleX | Self::AngleY | Self::AngleZ => VfxAttributeType::Float,
237            Self::AngularVelocityX | Self::AngularVelocityY | Self::AngularVelocityZ => VfxAttributeType::Float,
238            Self::TexIndex | Self::ParticleId | Self::SpawnIndex | Self::StripIndex => VfxAttributeType::Uint,
239            Self::PivotX | Self::PivotY | Self::PivotZ | Self::Random => VfxAttributeType::Float,
240        }
241    }
242}
243
244// ---------------------------------------------------------------------------
245// VFX Node port definitions
246// ---------------------------------------------------------------------------
247
248#[derive(Debug, Clone)]
249pub struct VfxPort {
250    pub name: String,
251    pub data_type: VfxAttributeType,
252    pub is_input: bool,
253    pub optional: bool,
254    pub default_value: VfxValue,
255    pub tooltip: String,
256}
257
258impl VfxPort {
259    pub fn input(name: &str, data_type: VfxAttributeType) -> Self {
260        let default_value = data_type.default_value();
261        Self {
262            name: name.to_string(),
263            data_type,
264            is_input: true,
265            optional: false,
266            default_value,
267            tooltip: String::new(),
268        }
269    }
270
271    pub fn output(name: &str, data_type: VfxAttributeType) -> Self {
272        let default_value = data_type.default_value();
273        Self {
274            name: name.to_string(),
275            data_type,
276            is_input: false,
277            optional: false,
278            default_value,
279            tooltip: String::new(),
280        }
281    }
282
283    pub fn optional(mut self) -> Self { self.optional = true; self }
284    pub fn with_tooltip(mut self, tip: &str) -> Self { self.tooltip = tip.to_string(); self }
285}
286
287// ---------------------------------------------------------------------------
288// VFX Node kinds / block library
289// ---------------------------------------------------------------------------
290
291#[derive(Debug, Clone, PartialEq)]
292pub enum VfxNodeKind {
293    // --- Contexts ---
294    SpawnContext,
295    InitializeContext,
296    UpdateContext,
297    OutputParticleQuad,
298    OutputParticleMesh,
299    OutputParticleStrip,
300
301    // --- Spawn operators ---
302    ConstantRate,
303    BurstSpawn,
304    PeriodicBurst,
305    OnCollision,
306    OnTrigger,
307    SpawnOnDeath,
308    SpawnFromPointCache,
309
310    // --- Initialize operators ---
311    SetPositionShape,
312    SetPositionSphere,
313    SetPositionCone,
314    SetPositionBox,
315    SetPositionLine,
316    SetPositionTorus,
317    SetPositionMeshSurface,
318    SetVelocityRandom,
319    SetVelocityTangent,
320    SetLifetime,
321    SetSize,
322    SetColor,
323    SetColorFromGradient,
324    SetAlpha,
325    SetMass,
326    SetAngle,
327    SetTexIndex,
328    InheritSourceVelocity,
329    InheritSourceColor,
330    InheritSourcePosition,
331
332    // --- Update operators ---
333    Gravity,
334    Drag,
335    Turbulence,
336    VelocityField,
337    ConformToSphere,
338    ConformToSdf,
339    OrbitForce,
340    LinearDrag,
341    AngularDrag,
342    AttractToPosition,
343    FlipbookAnimation,
344    RotateOverTime,
345    ScaleOverLife,
346    ColorOverLife,
347    AlphaOverLife,
348    SpeedLimiter,
349    Collision,
350    KillOnCollision,
351    KillOnBounds,
352    TriggerOnDeath,
353    UpdatePosition,
354    EulerIntegration,
355    Noise3D,
356    CurlNoise,
357
358    // --- Math nodes ---
359    Add,
360    Subtract,
361    Multiply,
362    Divide,
363    Power,
364    Sqrt,
365    Abs,
366    Sin,
367    Cos,
368    Tan,
369    Atan2,
370    Floor,
371    Ceil,
372    Round,
373    Frac,
374    Clamp,
375    Lerp,
376    Step,
377    SmoothStep,
378    Min,
379    Max,
380    Remap,
381    Dot,
382    Cross,
383    Normalize,
384    Length,
385    Distance,
386    Swizzle,
387    Combine,
388    Split,
389    Negate,
390    OneMinus,
391    Reciprocal,
392
393    // --- Sample operators ---
394    SampleGradient,
395    SampleCurve,
396    SampleTexture2D,
397    SampleTexture3D,
398    SampleSdf,
399    SampleMesh,
400    SampleNoise2D,
401    SampleNoise3D,
402
403    // --- Attribute nodes ---
404    GetAttribute,
405    SetAttribute,
406    GetBuiltin,
407
408    // --- Flow control ---
409    Branch,
410    Loop,
411    Random,
412    RandomPerComponent,
413    Sequence,
414
415    // --- Utility ---
416    Comment,
417    Sticky,
418    ExposedParameter,
419    CustomHlsl,
420}
421
422impl VfxNodeKind {
423    pub fn label(&self) -> &'static str {
424        match self {
425            Self::SpawnContext => "Spawn",
426            Self::InitializeContext => "Initialize",
427            Self::UpdateContext => "Update",
428            Self::OutputParticleQuad => "Output Particle (Quad)",
429            Self::OutputParticleMesh => "Output Particle (Mesh)",
430            Self::OutputParticleStrip => "Output Particle (Strip)",
431            Self::ConstantRate => "Constant Rate",
432            Self::BurstSpawn => "Burst",
433            Self::PeriodicBurst => "Periodic Burst",
434            Self::OnCollision => "On Collision",
435            Self::OnTrigger => "On Trigger",
436            Self::SpawnOnDeath => "Spawn on Death",
437            Self::SpawnFromPointCache => "Spawn from Point Cache",
438            Self::SetPositionShape => "Set Position (Shape)",
439            Self::SetPositionSphere => "Set Position (Sphere)",
440            Self::SetPositionCone => "Set Position (Cone)",
441            Self::SetPositionBox => "Set Position (Box)",
442            Self::SetPositionLine => "Set Position (Line)",
443            Self::SetPositionTorus => "Set Position (Torus)",
444            Self::SetPositionMeshSurface => "Set Position (Mesh Surface)",
445            Self::SetVelocityRandom => "Set Velocity (Random)",
446            Self::SetVelocityTangent => "Set Velocity (Tangent)",
447            Self::SetLifetime => "Set Lifetime",
448            Self::SetSize => "Set Size",
449            Self::SetColor => "Set Color",
450            Self::SetColorFromGradient => "Set Color (Gradient)",
451            Self::SetAlpha => "Set Alpha",
452            Self::SetMass => "Set Mass",
453            Self::SetAngle => "Set Angle",
454            Self::SetTexIndex => "Set Tex Index",
455            Self::InheritSourceVelocity => "Inherit Source Velocity",
456            Self::InheritSourceColor => "Inherit Source Color",
457            Self::InheritSourcePosition => "Inherit Source Position",
458            Self::Gravity => "Gravity",
459            Self::Drag => "Drag",
460            Self::Turbulence => "Turbulence",
461            Self::VelocityField => "Velocity Field",
462            Self::ConformToSphere => "Conform to Sphere",
463            Self::ConformToSdf => "Conform to SDF",
464            Self::OrbitForce => "Orbit Force",
465            Self::LinearDrag => "Linear Drag",
466            Self::AngularDrag => "Angular Drag",
467            Self::AttractToPosition => "Attract to Position",
468            Self::FlipbookAnimation => "Flipbook",
469            Self::RotateOverTime => "Rotate over Time",
470            Self::ScaleOverLife => "Scale over Life",
471            Self::ColorOverLife => "Color over Life",
472            Self::AlphaOverLife => "Alpha over Life",
473            Self::SpeedLimiter => "Speed Limiter",
474            Self::Collision => "Collision",
475            Self::KillOnCollision => "Kill on Collision",
476            Self::KillOnBounds => "Kill on Bounds",
477            Self::TriggerOnDeath => "Trigger on Death",
478            Self::UpdatePosition => "Update Position",
479            Self::EulerIntegration => "Euler Integration",
480            Self::Noise3D => "Noise 3D",
481            Self::CurlNoise => "Curl Noise",
482            Self::Add => "Add",
483            Self::Subtract => "Subtract",
484            Self::Multiply => "Multiply",
485            Self::Divide => "Divide",
486            Self::Power => "Power",
487            Self::Sqrt => "Sqrt",
488            Self::Abs => "Abs",
489            Self::Sin => "Sin",
490            Self::Cos => "Cos",
491            Self::Tan => "Tan",
492            Self::Atan2 => "Atan2",
493            Self::Floor => "Floor",
494            Self::Ceil => "Ceil",
495            Self::Round => "Round",
496            Self::Frac => "Frac",
497            Self::Clamp => "Clamp",
498            Self::Lerp => "Lerp",
499            Self::Step => "Step",
500            Self::SmoothStep => "Smooth Step",
501            Self::Min => "Min",
502            Self::Max => "Max",
503            Self::Remap => "Remap",
504            Self::Dot => "Dot Product",
505            Self::Cross => "Cross Product",
506            Self::Normalize => "Normalize",
507            Self::Length => "Length",
508            Self::Distance => "Distance",
509            Self::Swizzle => "Swizzle",
510            Self::Combine => "Combine",
511            Self::Split => "Split",
512            Self::Negate => "Negate",
513            Self::OneMinus => "One Minus",
514            Self::Reciprocal => "Reciprocal",
515            Self::SampleGradient => "Sample Gradient",
516            Self::SampleCurve => "Sample Curve",
517            Self::SampleTexture2D => "Sample Texture 2D",
518            Self::SampleTexture3D => "Sample Texture 3D",
519            Self::SampleSdf => "Sample SDF",
520            Self::SampleMesh => "Sample Mesh",
521            Self::SampleNoise2D => "Sample Noise 2D",
522            Self::SampleNoise3D => "Sample Noise 3D",
523            Self::GetAttribute => "Get Attribute",
524            Self::SetAttribute => "Set Attribute",
525            Self::GetBuiltin => "Get Builtin Attribute",
526            Self::Branch => "Branch",
527            Self::Loop => "Loop",
528            Self::Random => "Random",
529            Self::RandomPerComponent => "Random (Per Component)",
530            Self::Sequence => "Sequence",
531            Self::Comment => "Comment",
532            Self::Sticky => "Sticky Note",
533            Self::ExposedParameter => "Exposed Parameter",
534            Self::CustomHlsl => "Custom HLSL",
535        }
536    }
537
538    pub fn category(&self) -> &'static str {
539        match self {
540            Self::SpawnContext | Self::InitializeContext | Self::UpdateContext
541            | Self::OutputParticleQuad | Self::OutputParticleMesh | Self::OutputParticleStrip => "Context",
542            Self::ConstantRate | Self::BurstSpawn | Self::PeriodicBurst
543            | Self::OnCollision | Self::OnTrigger | Self::SpawnOnDeath | Self::SpawnFromPointCache => "Spawn",
544            Self::SetPositionShape | Self::SetPositionSphere | Self::SetPositionCone
545            | Self::SetPositionBox | Self::SetPositionLine | Self::SetPositionTorus
546            | Self::SetPositionMeshSurface | Self::SetVelocityRandom | Self::SetVelocityTangent
547            | Self::SetLifetime | Self::SetSize | Self::SetColor | Self::SetColorFromGradient
548            | Self::SetAlpha | Self::SetMass | Self::SetAngle | Self::SetTexIndex
549            | Self::InheritSourceVelocity | Self::InheritSourceColor | Self::InheritSourcePosition => "Initialize",
550            Self::Gravity | Self::Drag | Self::Turbulence | Self::VelocityField
551            | Self::ConformToSphere | Self::ConformToSdf | Self::OrbitForce | Self::LinearDrag
552            | Self::AngularDrag | Self::AttractToPosition | Self::FlipbookAnimation
553            | Self::RotateOverTime | Self::ScaleOverLife | Self::ColorOverLife | Self::AlphaOverLife
554            | Self::SpeedLimiter | Self::Collision | Self::KillOnCollision | Self::KillOnBounds
555            | Self::TriggerOnDeath | Self::UpdatePosition | Self::EulerIntegration
556            | Self::Noise3D | Self::CurlNoise => "Update",
557            Self::Add | Self::Subtract | Self::Multiply | Self::Divide | Self::Power
558            | Self::Sqrt | Self::Abs | Self::Sin | Self::Cos | Self::Tan | Self::Atan2
559            | Self::Floor | Self::Ceil | Self::Round | Self::Frac | Self::Clamp
560            | Self::Lerp | Self::Step | Self::SmoothStep | Self::Min | Self::Max
561            | Self::Remap | Self::Dot | Self::Cross | Self::Normalize | Self::Length
562            | Self::Distance | Self::Swizzle | Self::Combine | Self::Split
563            | Self::Negate | Self::OneMinus | Self::Reciprocal => "Math",
564            Self::SampleGradient | Self::SampleCurve | Self::SampleTexture2D
565            | Self::SampleTexture3D | Self::SampleSdf | Self::SampleMesh
566            | Self::SampleNoise2D | Self::SampleNoise3D => "Sample",
567            Self::GetAttribute | Self::SetAttribute | Self::GetBuiltin => "Attribute",
568            Self::Branch | Self::Loop | Self::Random | Self::RandomPerComponent | Self::Sequence => "Flow",
569            Self::Comment | Self::Sticky | Self::ExposedParameter | Self::CustomHlsl => "Utility",
570        }
571    }
572
573    pub fn header_color(&self) -> Vec4 {
574        match self.category() {
575            "Context" => Vec4::new(0.16, 0.29, 0.48, 1.0),
576            "Spawn" => Vec4::new(0.29, 0.48, 0.16, 1.0),
577            "Initialize" => Vec4::new(0.16, 0.48, 0.35, 1.0),
578            "Update" => Vec4::new(0.48, 0.29, 0.16, 1.0),
579            "Math" => Vec4::new(0.35, 0.22, 0.48, 1.0),
580            "Sample" => Vec4::new(0.48, 0.16, 0.35, 1.0),
581            "Attribute" => Vec4::new(0.22, 0.40, 0.48, 1.0),
582            "Flow" => Vec4::new(0.48, 0.40, 0.16, 1.0),
583            _ => Vec4::new(0.30, 0.30, 0.30, 1.0),
584        }
585    }
586
587    pub fn default_ports(&self) -> (Vec<VfxPort>, Vec<VfxPort>) {
588        match self {
589            Self::ConstantRate => (
590                vec![VfxPort::input("Rate", VfxAttributeType::Float)],
591                vec![VfxPort::output("Spawn Count", VfxAttributeType::Uint)],
592            ),
593            Self::SetLifetime => (
594                vec![VfxPort::input("Lifetime", VfxAttributeType::Float)],
595                vec![],
596            ),
597            Self::SetColor => (
598                vec![VfxPort::input("Color", VfxAttributeType::Color)],
599                vec![],
600            ),
601            Self::SetSize => (
602                vec![VfxPort::input("Size", VfxAttributeType::Float)],
603                vec![],
604            ),
605            Self::Add | Self::Subtract | Self::Multiply | Self::Divide => (
606                vec![
607                    VfxPort::input("A", VfxAttributeType::Float),
608                    VfxPort::input("B", VfxAttributeType::Float),
609                ],
610                vec![VfxPort::output("Result", VfxAttributeType::Float)],
611            ),
612            Self::Lerp => (
613                vec![
614                    VfxPort::input("A", VfxAttributeType::Float),
615                    VfxPort::input("B", VfxAttributeType::Float),
616                    VfxPort::input("T", VfxAttributeType::Float),
617                ],
618                vec![VfxPort::output("Result", VfxAttributeType::Float)],
619            ),
620            Self::Clamp => (
621                vec![
622                    VfxPort::input("Value", VfxAttributeType::Float),
623                    VfxPort::input("Min", VfxAttributeType::Float),
624                    VfxPort::input("Max", VfxAttributeType::Float),
625                ],
626                vec![VfxPort::output("Result", VfxAttributeType::Float)],
627            ),
628            Self::SampleGradient => (
629                vec![
630                    VfxPort::input("Gradient", VfxAttributeType::Gradient),
631                    VfxPort::input("T", VfxAttributeType::Float),
632                ],
633                vec![VfxPort::output("Color", VfxAttributeType::Color)],
634            ),
635            Self::SampleCurve => (
636                vec![
637                    VfxPort::input("Curve", VfxAttributeType::AnimationCurve),
638                    VfxPort::input("T", VfxAttributeType::Float),
639                ],
640                vec![VfxPort::output("Value", VfxAttributeType::Float)],
641            ),
642            Self::GetBuiltin => (
643                vec![],
644                vec![VfxPort::output("Value", VfxAttributeType::Float)],
645            ),
646            Self::Gravity => (
647                vec![VfxPort::input("Gravity", VfxAttributeType::Float3).optional()],
648                vec![],
649            ),
650            Self::Noise3D => (
651                vec![
652                    VfxPort::input("Position", VfxAttributeType::Float3),
653                    VfxPort::input("Frequency", VfxAttributeType::Float),
654                    VfxPort::input("Octaves", VfxAttributeType::Int).optional(),
655                ],
656                vec![
657                    VfxPort::output("Noise", VfxAttributeType::Float),
658                    VfxPort::output("Noise3D", VfxAttributeType::Float3),
659                ],
660            ),
661            Self::Branch => (
662                vec![
663                    VfxPort::input("Condition", VfxAttributeType::Bool),
664                    VfxPort::input("True", VfxAttributeType::Float),
665                    VfxPort::input("False", VfxAttributeType::Float),
666                ],
667                vec![VfxPort::output("Result", VfxAttributeType::Float)],
668            ),
669            Self::Combine => (
670                vec![
671                    VfxPort::input("X", VfxAttributeType::Float),
672                    VfxPort::input("Y", VfxAttributeType::Float),
673                    VfxPort::input("Z", VfxAttributeType::Float).optional(),
674                    VfxPort::input("W", VfxAttributeType::Float).optional(),
675                ],
676                vec![VfxPort::output("Vector", VfxAttributeType::Float4)],
677            ),
678            Self::Split => (
679                vec![VfxPort::input("Vector", VfxAttributeType::Float4)],
680                vec![
681                    VfxPort::output("X", VfxAttributeType::Float),
682                    VfxPort::output("Y", VfxAttributeType::Float),
683                    VfxPort::output("Z", VfxAttributeType::Float),
684                    VfxPort::output("W", VfxAttributeType::Float),
685                ],
686            ),
687            _ => (vec![], vec![]),
688        }
689    }
690}
691
692// ---------------------------------------------------------------------------
693// VFX Graph Node
694// ---------------------------------------------------------------------------
695
696static mut VFX_NODE_ID_COUNTER: u64 = 1;
697fn next_vfx_node_id() -> u64 {
698    unsafe {
699        let id = VFX_NODE_ID_COUNTER;
700        VFX_NODE_ID_COUNTER += 1;
701        id
702    }
703}
704
705#[derive(Debug, Clone)]
706pub struct VfxNode {
707    pub id: u64,
708    pub kind: VfxNodeKind,
709    pub position: Vec2,
710    pub size: Vec2,
711    pub title_override: Option<String>,
712    pub inputs: Vec<VfxPort>,
713    pub outputs: Vec<VfxPort>,
714    pub param_values: HashMap<String, VfxValue>,
715    pub comment: String,
716    pub collapsed: bool,
717    pub enabled: bool,
718    pub preview_enabled: bool,
719    pub error_message: Option<String>,
720    pub custom_hlsl_code: String,
721    pub exposed_param_name: String,
722    pub builtin_attribute: BuiltinAttribute,
723}
724
725impl VfxNode {
726    pub fn new(kind: VfxNodeKind, position: Vec2) -> Self {
727        let (inputs, outputs) = kind.default_ports();
728        let mut node = Self {
729            id: next_vfx_node_id(),
730            kind,
731            position,
732            size: Vec2::new(200.0, 80.0),
733            title_override: None,
734            inputs,
735            outputs,
736            param_values: HashMap::new(),
737            comment: String::new(),
738            collapsed: false,
739            enabled: true,
740            preview_enabled: false,
741            error_message: None,
742            custom_hlsl_code: String::new(),
743            exposed_param_name: String::new(),
744            builtin_attribute: BuiltinAttribute::Age,
745        };
746        node.recalculate_size();
747        node
748    }
749
750    pub fn label(&self) -> &str {
751        if let Some(ref t) = self.title_override {
752            t.as_str()
753        } else {
754            self.kind.label()
755        }
756    }
757
758    pub fn recalculate_size(&mut self) {
759        let port_count = self.inputs.len().max(self.outputs.len());
760        let header_h = 28.0;
761        let port_h = 22.0;
762        let footer_h = 4.0;
763        self.size.x = 200.0;
764        self.size.y = header_h + port_count as f32 * port_h + footer_h;
765        if self.size.y < 60.0 { self.size.y = 60.0; }
766    }
767
768    pub fn input_port_position(&self, index: usize) -> Vec2 {
769        let y = self.position.y + 28.0 + index as f32 * 22.0 + 11.0;
770        Vec2::new(self.position.x, y)
771    }
772
773    pub fn output_port_position(&self, index: usize) -> Vec2 {
774        let y = self.position.y + 28.0 + index as f32 * 22.0 + 11.0;
775        Vec2::new(self.position.x + self.size.x, y)
776    }
777
778    pub fn contains_point(&self, pt: Vec2) -> bool {
779        pt.x >= self.position.x && pt.x <= self.position.x + self.size.x
780            && pt.y >= self.position.y && pt.y <= self.position.y + self.size.y
781    }
782
783    pub fn set_param(&mut self, name: &str, value: VfxValue) {
784        self.param_values.insert(name.to_string(), value);
785    }
786
787    pub fn get_param(&self, name: &str) -> Option<&VfxValue> {
788        self.param_values.get(name)
789    }
790}
791
792// ---------------------------------------------------------------------------
793// VFX Connection
794// ---------------------------------------------------------------------------
795
796#[derive(Debug, Clone)]
797pub struct VfxConnection {
798    pub id: u64,
799    pub from_node: u64,
800    pub from_port: usize,
801    pub to_node: u64,
802    pub to_port: usize,
803}
804
805// ---------------------------------------------------------------------------
806// Context block (groups of nodes within a context)
807// ---------------------------------------------------------------------------
808
809#[derive(Debug, Clone, Copy, PartialEq)]
810pub enum VfxContextKind {
811    Spawn,
812    Initialize,
813    Update,
814    Output,
815}
816
817#[derive(Debug, Clone)]
818pub struct VfxContext {
819    pub id: u64,
820    pub kind: VfxContextKind,
821    pub node_id: u64,
822    pub operator_node_ids: Vec<u64>,
823    pub enabled: bool,
824    pub capacity: u32,
825    pub label: String,
826}
827
828impl VfxContext {
829    pub fn new(kind: VfxContextKind, node_id: u64) -> Self {
830        let label = match kind {
831            VfxContextKind::Spawn => "Spawn".to_string(),
832            VfxContextKind::Initialize => "Initialize".to_string(),
833            VfxContextKind::Update => "Update".to_string(),
834            VfxContextKind::Output => "Output".to_string(),
835        };
836        Self {
837            id: next_vfx_node_id(),
838            kind,
839            node_id,
840            operator_node_ids: Vec::new(),
841            enabled: true,
842            capacity: 4096,
843            label,
844        }
845    }
846}
847
848// ---------------------------------------------------------------------------
849// Exposed parameter
850// ---------------------------------------------------------------------------
851
852#[derive(Debug, Clone)]
853pub struct ExposedParameter {
854    pub name: String,
855    pub data_type: VfxAttributeType,
856    pub value: VfxValue,
857    pub min: f32,
858    pub max: f32,
859    pub tooltip: String,
860    pub exposed: bool,
861}
862
863impl ExposedParameter {
864    pub fn new(name: &str, data_type: VfxAttributeType) -> Self {
865        let value = data_type.default_value();
866        Self {
867            name: name.to_string(),
868            data_type,
869            value,
870            min: 0.0,
871            max: 1.0,
872            tooltip: String::new(),
873            exposed: true,
874        }
875    }
876}
877
878// ---------------------------------------------------------------------------
879// VFX Graph
880// ---------------------------------------------------------------------------
881
882#[derive(Debug, Clone)]
883pub struct VfxGraph {
884    pub name: String,
885    pub nodes: Vec<VfxNode>,
886    pub connections: Vec<VfxConnection>,
887    pub contexts: Vec<VfxContext>,
888    pub exposed_parameters: Vec<ExposedParameter>,
889    pub next_conn_id: u64,
890    pub dirty: bool,
891    pub last_compile_error: Option<String>,
892    pub compile_warnings: Vec<String>,
893    pub particle_capacity: u32,
894    pub simulation_space: SimulationSpace,
895    pub culling_mode: CullingMode,
896}
897
898#[derive(Debug, Clone, Copy, PartialEq)]
899pub enum SimulationSpace { Local, World }
900#[derive(Debug, Clone, Copy, PartialEq)]
901pub enum CullingMode { Automatic, AlwaysSimulate, StopSimulating, Pause }
902
903impl VfxGraph {
904    pub fn new(name: &str) -> Self {
905        Self {
906            name: name.to_string(),
907            nodes: Vec::new(),
908            connections: Vec::new(),
909            contexts: Vec::new(),
910            exposed_parameters: Vec::new(),
911            next_conn_id: 1,
912            dirty: false,
913            last_compile_error: None,
914            compile_warnings: Vec::new(),
915            particle_capacity: 4096,
916            simulation_space: SimulationSpace::World,
917            culling_mode: CullingMode::Automatic,
918        }
919    }
920
921    pub fn add_node(&mut self, node: VfxNode) -> u64 {
922        let id = node.id;
923        self.nodes.push(node);
924        self.dirty = true;
925        id
926    }
927
928    pub fn remove_node(&mut self, id: u64) {
929        self.nodes.retain(|n| n.id != id);
930        self.connections.retain(|c| c.from_node != id && c.to_node != id);
931        for ctx in &mut self.contexts {
932            ctx.operator_node_ids.retain(|&nid| nid != id);
933        }
934        self.dirty = true;
935    }
936
937    pub fn find_node(&self, id: u64) -> Option<&VfxNode> {
938        self.nodes.iter().find(|n| n.id == id)
939    }
940
941    pub fn find_node_mut(&mut self, id: u64) -> Option<&mut VfxNode> {
942        self.nodes.iter_mut().find(|n| n.id == id)
943    }
944
945    pub fn connect(&mut self, from_node: u64, from_port: usize, to_node: u64, to_port: usize) -> u64 {
946        // Remove existing connection to the same input port
947        self.connections.retain(|c| !(c.to_node == to_node && c.to_port == to_port));
948        let id = self.next_conn_id;
949        self.next_conn_id += 1;
950        self.connections.push(VfxConnection { id, from_node, from_port, to_node, to_port });
951        self.dirty = true;
952        id
953    }
954
955    pub fn disconnect(&mut self, conn_id: u64) {
956        self.connections.retain(|c| c.id != conn_id);
957        self.dirty = true;
958    }
959
960    pub fn validate(&mut self) -> Vec<String> {
961        let mut errors = Vec::new();
962        let node_ids: std::collections::HashSet<u64> = self.nodes.iter().map(|n| n.id).collect();
963        for conn in &self.connections {
964            if !node_ids.contains(&conn.from_node) {
965                errors.push(format!("Connection {} references missing node {}", conn.id, conn.from_node));
966            }
967            if !node_ids.contains(&conn.to_node) {
968                errors.push(format!("Connection {} references missing node {}", conn.id, conn.to_node));
969            }
970        }
971        // Check all required inputs are connected
972        let connected_inputs: std::collections::HashSet<(u64, usize)> =
973            self.connections.iter().map(|c| (c.to_node, c.to_port)).collect();
974        for node in &self.nodes {
975            for (i, port) in node.inputs.iter().enumerate() {
976                if !port.optional && !connected_inputs.contains(&(node.id, i)) {
977                    // Only warn, not error (they may have inline values)
978                    self.compile_warnings.push(format!(
979                        "Node '{}' port '{}' has no connection (will use default)",
980                        node.label(), port.name
981                    ));
982                }
983            }
984        }
985        if errors.is_empty() {
986            self.last_compile_error = None;
987        } else {
988            self.last_compile_error = Some(errors.join("; "));
989        }
990        errors
991    }
992
993    pub fn topological_order(&self) -> Vec<u64> {
994        let mut in_degree: HashMap<u64, usize> = self.nodes.iter().map(|n| (n.id, 0)).collect();
995        let mut adjacency: HashMap<u64, Vec<u64>> = self.nodes.iter().map(|n| (n.id, Vec::new())).collect();
996        for conn in &self.connections {
997            *in_degree.entry(conn.to_node).or_insert(0) += 1;
998            adjacency.entry(conn.from_node).or_default().push(conn.to_node);
999        }
1000        let mut queue: std::collections::VecDeque<u64> = in_degree.iter()
1001            .filter(|(_, &deg)| deg == 0)
1002            .map(|(&id, _)| id)
1003            .collect();
1004        let mut order = Vec::new();
1005        while let Some(id) = queue.pop_front() {
1006            order.push(id);
1007            if let Some(neighbors) = adjacency.get(&id) {
1008                for &next in neighbors {
1009                    let deg = in_degree.entry(next).or_insert(0);
1010                    if *deg > 0 { *deg -= 1; }
1011                    if *deg == 0 { queue.push_back(next); }
1012                }
1013            }
1014        }
1015        order
1016    }
1017
1018    pub fn generate_hlsl(&self) -> String {
1019        let order = self.topological_order();
1020        let mut lines = Vec::new();
1021        lines.push("// Auto-generated VFX HLSL".to_string());
1022        lines.push("struct ParticleData {".to_string());
1023        lines.push("    float3 position;".to_string());
1024        lines.push("    float3 velocity;".to_string());
1025        lines.push("    float4 color;".to_string());
1026        lines.push("    float size;".to_string());
1027        lines.push("    float age;".to_string());
1028        lines.push("    float lifetime;".to_string());
1029        lines.push("};".to_string());
1030        lines.push(String::new());
1031        lines.push("[numthreads(64, 1, 1)]".to_string());
1032        lines.push("void VFXMain(uint id : SV_DispatchThreadID) {".to_string());
1033        lines.push("    ParticleData p = particleBuffer[id];".to_string());
1034        for &node_id in &order {
1035            if let Some(node) = self.find_node(node_id) {
1036                let snippet = self.node_to_hlsl(node);
1037                if !snippet.is_empty() {
1038                    lines.push(format!("    // {}", node.label()));
1039                    lines.push(format!("    {}", snippet));
1040                }
1041            }
1042        }
1043        lines.push("    particleBuffer[id] = p;".to_string());
1044        lines.push("}".to_string());
1045        lines.join("\n")
1046    }
1047
1048    fn node_to_hlsl(&self, node: &VfxNode) -> String {
1049        match node.kind {
1050            VfxNodeKind::Gravity => {
1051                let g = node.get_param("Gravity")
1052                    .map(|v| v.as_float3())
1053                    .unwrap_or(Vec3::new(0.0, -9.81, 0.0));
1054                format!("p.velocity += float3({}, {}, {}) * deltaTime;", g.x, g.y, g.z)
1055            }
1056            VfxNodeKind::Drag => {
1057                let drag = node.get_param("Drag")
1058                    .map(|v| v.as_float())
1059                    .unwrap_or(0.1);
1060                format!("p.velocity *= (1.0 - {:.4} * deltaTime);", drag)
1061            }
1062            VfxNodeKind::EulerIntegration => {
1063                "p.position += p.velocity * deltaTime;".to_string()
1064            }
1065            VfxNodeKind::ScaleOverLife => {
1066                "p.size = lerp(p.size, 0.0, saturate(p.age / p.lifetime));".to_string()
1067            }
1068            VfxNodeKind::AlphaOverLife => {
1069                "p.color.a = saturate(1.0 - p.age / p.lifetime);".to_string()
1070            }
1071            VfxNodeKind::CustomHlsl => node.custom_hlsl_code.clone(),
1072            _ => String::new(),
1073        }
1074    }
1075
1076    pub fn estimate_memory_bytes(&self) -> usize {
1077        // Per-particle: position(12) + velocity(12) + color(16) + size(4) + age(4) + lifetime(4) = 52 bytes
1078        self.particle_capacity as usize * 52
1079    }
1080
1081    pub fn build_fire_effect() -> Self {
1082        let mut graph = Self::new("Fire");
1083        graph.particle_capacity = 2048;
1084
1085        let spawn = graph.add_node(VfxNode::new(VfxNodeKind::SpawnContext, Vec2::new(20.0, 20.0)));
1086        let rate = graph.add_node(VfxNode::new(VfxNodeKind::ConstantRate, Vec2::new(20.0, 80.0)));
1087        if let Some(n) = graph.find_node_mut(rate) {
1088            n.set_param("Rate", VfxValue::Float(50.0));
1089        }
1090
1091        let init = graph.add_node(VfxNode::new(VfxNodeKind::InitializeContext, Vec2::new(300.0, 20.0)));
1092        let set_pos = graph.add_node(VfxNode::new(VfxNodeKind::SetPositionCone, Vec2::new(300.0, 80.0)));
1093        let set_life = graph.add_node(VfxNode::new(VfxNodeKind::SetLifetime, Vec2::new(300.0, 140.0)));
1094        if let Some(n) = graph.find_node_mut(set_life) {
1095            n.set_param("Lifetime", VfxValue::Float(1.5));
1096        }
1097        let set_color = graph.add_node(VfxNode::new(VfxNodeKind::SetColor, Vec2::new(300.0, 200.0)));
1098        if let Some(n) = graph.find_node_mut(set_color) {
1099            n.set_param("Color", VfxValue::Color(Vec4::new(1.0, 0.4, 0.0, 1.0)));
1100        }
1101
1102        let update = graph.add_node(VfxNode::new(VfxNodeKind::UpdateContext, Vec2::new(580.0, 20.0)));
1103        let gravity_node = graph.add_node(VfxNode::new(VfxNodeKind::Gravity, Vec2::new(580.0, 80.0)));
1104        if let Some(n) = graph.find_node_mut(gravity_node) {
1105            n.set_param("Gravity", VfxValue::Float3(Vec3::new(0.0, -2.0, 0.0)));
1106        }
1107        let turbulence = graph.add_node(VfxNode::new(VfxNodeKind::Turbulence, Vec2::new(580.0, 140.0)));
1108        let color_life = graph.add_node(VfxNode::new(VfxNodeKind::ColorOverLife, Vec2::new(580.0, 200.0)));
1109        let alpha_life = graph.add_node(VfxNode::new(VfxNodeKind::AlphaOverLife, Vec2::new(580.0, 260.0)));
1110        let euler = graph.add_node(VfxNode::new(VfxNodeKind::EulerIntegration, Vec2::new(580.0, 320.0)));
1111
1112        let output = graph.add_node(VfxNode::new(VfxNodeKind::OutputParticleQuad, Vec2::new(860.0, 20.0)));
1113
1114        // Wire spawn -> rate
1115        graph.connect(rate, 0, spawn, 0);
1116        // Wire contexts (conceptual ordering)
1117        graph.connect(spawn, 0, init, 0);
1118        graph.connect(init, 0, update, 0);
1119        graph.connect(update, 0, output, 0);
1120        // Operators
1121        graph.connect(set_pos, 0, init, 1);
1122        graph.connect(set_life, 0, init, 2);
1123        graph.connect(set_color, 0, init, 3);
1124        graph.connect(gravity_node, 0, update, 1);
1125        graph.connect(turbulence, 0, update, 2);
1126        graph.connect(color_life, 0, update, 3);
1127        graph.connect(alpha_life, 0, update, 4);
1128        graph.connect(euler, 0, update, 5);
1129
1130        graph
1131    }
1132
1133    pub fn build_smoke_effect() -> Self {
1134        let mut graph = Self::new("Smoke");
1135        graph.particle_capacity = 1024;
1136
1137        let spawn = graph.add_node(VfxNode::new(VfxNodeKind::SpawnContext, Vec2::new(20.0, 20.0)));
1138        let rate = graph.add_node(VfxNode::new(VfxNodeKind::ConstantRate, Vec2::new(20.0, 80.0)));
1139        if let Some(n) = graph.find_node_mut(rate) {
1140            n.set_param("Rate", VfxValue::Float(15.0));
1141        }
1142
1143        let init = graph.add_node(VfxNode::new(VfxNodeKind::InitializeContext, Vec2::new(300.0, 20.0)));
1144        let set_pos = graph.add_node(VfxNode::new(VfxNodeKind::SetPositionSphere, Vec2::new(300.0, 80.0)));
1145        let set_life = graph.add_node(VfxNode::new(VfxNodeKind::SetLifetime, Vec2::new(300.0, 140.0)));
1146        if let Some(n) = graph.find_node_mut(set_life) {
1147            n.set_param("Lifetime", VfxValue::Float(4.0));
1148        }
1149        let set_size = graph.add_node(VfxNode::new(VfxNodeKind::SetSize, Vec2::new(300.0, 200.0)));
1150        if let Some(n) = graph.find_node_mut(set_size) {
1151            n.set_param("Size", VfxValue::Float(0.5));
1152        }
1153        let set_color = graph.add_node(VfxNode::new(VfxNodeKind::SetColor, Vec2::new(300.0, 260.0)));
1154        if let Some(n) = graph.find_node_mut(set_color) {
1155            n.set_param("Color", VfxValue::Color(Vec4::new(0.6, 0.6, 0.6, 0.8)));
1156        }
1157
1158        let update = graph.add_node(VfxNode::new(VfxNodeKind::UpdateContext, Vec2::new(580.0, 20.0)));
1159        let gravity_node = graph.add_node(VfxNode::new(VfxNodeKind::Gravity, Vec2::new(580.0, 80.0)));
1160        if let Some(n) = graph.find_node_mut(gravity_node) {
1161            n.set_param("Gravity", VfxValue::Float3(Vec3::new(0.0, 0.5, 0.0)));
1162        }
1163        let scale_life = graph.add_node(VfxNode::new(VfxNodeKind::ScaleOverLife, Vec2::new(580.0, 140.0)));
1164        let alpha_life = graph.add_node(VfxNode::new(VfxNodeKind::AlphaOverLife, Vec2::new(580.0, 200.0)));
1165        let euler = graph.add_node(VfxNode::new(VfxNodeKind::EulerIntegration, Vec2::new(580.0, 260.0)));
1166
1167        let output = graph.add_node(VfxNode::new(VfxNodeKind::OutputParticleQuad, Vec2::new(860.0, 20.0)));
1168
1169        graph.connect(rate, 0, spawn, 0);
1170        graph.connect(spawn, 0, init, 0);
1171        graph.connect(init, 0, update, 0);
1172        graph.connect(update, 0, output, 0);
1173        graph.connect(set_pos, 0, init, 1);
1174        graph.connect(set_life, 0, init, 2);
1175        graph.connect(set_size, 0, init, 3);
1176        graph.connect(set_color, 0, init, 4);
1177        graph.connect(gravity_node, 0, update, 1);
1178        graph.connect(scale_life, 0, update, 2);
1179        graph.connect(alpha_life, 0, update, 3);
1180        graph.connect(euler, 0, update, 4);
1181
1182        graph
1183    }
1184
1185    pub fn build_sparks_effect() -> Self {
1186        let mut graph = Self::new("Sparks");
1187        graph.particle_capacity = 8192;
1188
1189        let spawn = graph.add_node(VfxNode::new(VfxNodeKind::SpawnContext, Vec2::new(20.0, 20.0)));
1190        let burst = graph.add_node(VfxNode::new(VfxNodeKind::BurstSpawn, Vec2::new(20.0, 80.0)));
1191        if let Some(n) = graph.find_node_mut(burst) {
1192            n.set_param("Count", VfxValue::Uint(200));
1193        }
1194
1195        let init = graph.add_node(VfxNode::new(VfxNodeKind::InitializeContext, Vec2::new(300.0, 20.0)));
1196        let set_pos = graph.add_node(VfxNode::new(VfxNodeKind::SetPositionShape, Vec2::new(300.0, 80.0)));
1197        let set_vel = graph.add_node(VfxNode::new(VfxNodeKind::SetVelocityRandom, Vec2::new(300.0, 140.0)));
1198        let set_life = graph.add_node(VfxNode::new(VfxNodeKind::SetLifetime, Vec2::new(300.0, 200.0)));
1199        if let Some(n) = graph.find_node_mut(set_life) {
1200            n.set_param("Lifetime", VfxValue::Float(0.8));
1201        }
1202        let set_size = graph.add_node(VfxNode::new(VfxNodeKind::SetSize, Vec2::new(300.0, 260.0)));
1203        if let Some(n) = graph.find_node_mut(set_size) {
1204            n.set_param("Size", VfxValue::Float(0.05));
1205        }
1206        let set_color = graph.add_node(VfxNode::new(VfxNodeKind::SetColor, Vec2::new(300.0, 320.0)));
1207        if let Some(n) = graph.find_node_mut(set_color) {
1208            n.set_param("Color", VfxValue::Color(Vec4::new(1.0, 0.9, 0.3, 1.0)));
1209        }
1210
1211        let update = graph.add_node(VfxNode::new(VfxNodeKind::UpdateContext, Vec2::new(580.0, 20.0)));
1212        let gravity_node = graph.add_node(VfxNode::new(VfxNodeKind::Gravity, Vec2::new(580.0, 80.0)));
1213        let drag_node = graph.add_node(VfxNode::new(VfxNodeKind::Drag, Vec2::new(580.0, 140.0)));
1214        if let Some(n) = graph.find_node_mut(drag_node) {
1215            n.set_param("Drag", VfxValue::Float(2.0));
1216        }
1217        let alpha_life = graph.add_node(VfxNode::new(VfxNodeKind::AlphaOverLife, Vec2::new(580.0, 200.0)));
1218        let euler = graph.add_node(VfxNode::new(VfxNodeKind::EulerIntegration, Vec2::new(580.0, 260.0)));
1219
1220        let output = graph.add_node(VfxNode::new(VfxNodeKind::OutputParticleStrip, Vec2::new(860.0, 20.0)));
1221
1222        graph.connect(burst, 0, spawn, 0);
1223        graph.connect(spawn, 0, init, 0);
1224        graph.connect(init, 0, update, 0);
1225        graph.connect(update, 0, output, 0);
1226        graph.connect(set_pos, 0, init, 1);
1227        graph.connect(set_vel, 0, init, 2);
1228        graph.connect(set_life, 0, init, 3);
1229        graph.connect(set_size, 0, init, 4);
1230        graph.connect(set_color, 0, init, 5);
1231        graph.connect(gravity_node, 0, update, 1);
1232        graph.connect(drag_node, 0, update, 2);
1233        graph.connect(alpha_life, 0, update, 3);
1234        graph.connect(euler, 0, update, 4);
1235
1236        graph
1237    }
1238}
1239
1240// ---------------------------------------------------------------------------
1241// VFX Graph editor view state
1242// ---------------------------------------------------------------------------
1243
1244#[derive(Debug, Clone, Copy, PartialEq)]
1245pub enum VfxEditorTool {
1246    Select,
1247    Connect,
1248    Comment,
1249    Pan,
1250}
1251
1252#[derive(Debug, Clone, Copy, PartialEq)]
1253pub enum VfxEditorPanel {
1254    Graph,
1255    Inspector,
1256    Preview,
1257    Blackboard,
1258    Profiler,
1259}
1260
1261#[derive(Debug, Clone)]
1262pub struct VfxEditorView {
1263    pub zoom: f32,
1264    pub pan: Vec2,
1265    pub min_zoom: f32,
1266    pub max_zoom: f32,
1267    pub grid_size: f32,
1268    pub show_grid: bool,
1269    pub show_node_minimap: bool,
1270}
1271
1272impl Default for VfxEditorView {
1273    fn default() -> Self {
1274        Self {
1275            zoom: 1.0,
1276            pan: Vec2::ZERO,
1277            min_zoom: 0.1,
1278            max_zoom: 4.0,
1279            grid_size: 20.0,
1280            show_grid: true,
1281            show_node_minimap: true,
1282        }
1283    }
1284}
1285
1286impl VfxEditorView {
1287    pub fn screen_to_graph(&self, screen: Vec2) -> Vec2 {
1288        (screen - self.pan) / self.zoom
1289    }
1290
1291    pub fn graph_to_screen(&self, graph: Vec2) -> Vec2 {
1292        graph * self.zoom + self.pan
1293    }
1294
1295    pub fn zoom_at(&mut self, focus: Vec2, delta: f32) {
1296        let old_zoom = self.zoom;
1297        self.zoom = (self.zoom * (1.0 + delta * 0.1)).clamp(self.min_zoom, self.max_zoom);
1298        let scale = self.zoom / old_zoom;
1299        self.pan = focus + (self.pan - focus) * scale;
1300    }
1301
1302    pub fn snap_to_grid(&self, pos: Vec2) -> Vec2 {
1303        let g = self.grid_size;
1304        Vec2::new((pos.x / g).round() * g, (pos.y / g).round() * g)
1305    }
1306
1307    pub fn fit_to_graph(&mut self, nodes: &[VfxNode], viewport_size: Vec2) {
1308        if nodes.is_empty() { return; }
1309        let min_x = nodes.iter().map(|n| n.position.x).fold(f32::MAX, f32::min);
1310        let min_y = nodes.iter().map(|n| n.position.y).fold(f32::MAX, f32::min);
1311        let max_x = nodes.iter().map(|n| n.position.x + n.size.x).fold(f32::MIN, f32::max);
1312        let max_y = nodes.iter().map(|n| n.position.y + n.size.y).fold(f32::MIN, f32::max);
1313        let content_w = max_x - min_x + 80.0;
1314        let content_h = max_y - min_y + 80.0;
1315        let scale_x = viewport_size.x / content_w;
1316        let scale_y = viewport_size.y / content_h;
1317        self.zoom = scale_x.min(scale_y).clamp(self.min_zoom, self.max_zoom);
1318        let center = Vec2::new((min_x + max_x) * 0.5, (min_y + max_y) * 0.5);
1319        self.pan = viewport_size * 0.5 - center * self.zoom;
1320    }
1321}
1322
1323// ---------------------------------------------------------------------------
1324// Drag state
1325// ---------------------------------------------------------------------------
1326
1327#[derive(Debug, Clone)]
1328pub enum VfxDragState {
1329    None,
1330    DraggingNode { node_id: u64, offset: Vec2 },
1331    DraggingConnection { from_node: u64, from_port: usize, is_input: bool, current_pos: Vec2 },
1332    BoxSelect { start: Vec2, current: Vec2 },
1333    Panning { last: Vec2 },
1334}
1335
1336// ---------------------------------------------------------------------------
1337// GPU profiler data for VFX systems
1338// ---------------------------------------------------------------------------
1339
1340#[derive(Debug, Clone, Default)]
1341pub struct VfxProfilerSample {
1342    pub graph_name: String,
1343    pub spawn_ms: f32,
1344    pub init_ms: f32,
1345    pub update_ms: f32,
1346    pub render_ms: f32,
1347    pub particle_count: u32,
1348    pub alive_count: u32,
1349    pub memory_bytes: usize,
1350    pub draw_calls: u32,
1351}
1352
1353impl VfxProfilerSample {
1354    pub fn total_ms(&self) -> f32 {
1355        self.spawn_ms + self.init_ms + self.update_ms + self.render_ms
1356    }
1357
1358    pub fn memory_kb(&self) -> f32 {
1359        self.memory_bytes as f32 / 1024.0
1360    }
1361}
1362
1363#[derive(Debug, Clone)]
1364pub struct VfxProfiler {
1365    pub samples: Vec<VfxProfilerSample>,
1366    pub history_len: usize,
1367    pub paused: bool,
1368}
1369
1370impl Default for VfxProfiler {
1371    fn default() -> Self {
1372        Self {
1373            samples: Vec::new(),
1374            history_len: 128,
1375            paused: false,
1376        }
1377    }
1378}
1379
1380impl VfxProfiler {
1381    pub fn push_sample(&mut self, sample: VfxProfilerSample) {
1382        if self.paused { return; }
1383        self.samples.push(sample);
1384        if self.samples.len() > self.history_len {
1385            self.samples.remove(0);
1386        }
1387    }
1388
1389    pub fn avg_total_ms(&self) -> f32 {
1390        if self.samples.is_empty() { return 0.0; }
1391        self.samples.iter().map(|s| s.total_ms()).sum::<f32>() / self.samples.len() as f32
1392    }
1393
1394    pub fn peak_particle_count(&self) -> u32 {
1395        self.samples.iter().map(|s| s.alive_count).max().unwrap_or(0)
1396    }
1397}
1398
1399// ---------------------------------------------------------------------------
1400// VFX editor blackboard (shared variables)
1401// ---------------------------------------------------------------------------
1402
1403#[derive(Debug, Clone)]
1404pub struct VfxBlackboardEntry {
1405    pub name: String,
1406    pub value: VfxValue,
1407    pub description: String,
1408    pub exposed: bool,
1409}
1410
1411#[derive(Debug, Clone, Default)]
1412pub struct VfxBlackboard {
1413    pub entries: Vec<VfxBlackboardEntry>,
1414}
1415
1416impl VfxBlackboard {
1417    pub fn add(&mut self, name: &str, value: VfxValue) {
1418        self.entries.push(VfxBlackboardEntry {
1419            name: name.to_string(),
1420            value,
1421            description: String::new(),
1422            exposed: false,
1423        });
1424    }
1425
1426    pub fn get(&self, name: &str) -> Option<&VfxValue> {
1427        self.entries.iter().find(|e| e.name == name).map(|e| &e.value)
1428    }
1429
1430    pub fn set(&mut self, name: &str, value: VfxValue) {
1431        if let Some(e) = self.entries.iter_mut().find(|e| e.name == name) {
1432            e.value = value;
1433        } else {
1434            self.add(name, value);
1435        }
1436    }
1437}
1438
1439// ---------------------------------------------------------------------------
1440// VFX instance runtime data
1441// ---------------------------------------------------------------------------
1442
1443#[derive(Debug, Clone)]
1444pub struct VfxInstance {
1445    pub graph_name: String,
1446    pub position: Vec3,
1447    pub rotation: Quat,
1448    pub scale: Vec3,
1449    pub playing: bool,
1450    pub looping: bool,
1451    pub play_time: f32,
1452    pub duration: f32,
1453    pub speed: f32,
1454    pub prewarm: bool,
1455    pub seed: u32,
1456    pub attached_to_entity: Option<u64>,
1457}
1458
1459impl VfxInstance {
1460    pub fn new(graph_name: &str) -> Self {
1461        Self {
1462            graph_name: graph_name.to_string(),
1463            position: Vec3::ZERO,
1464            rotation: Quat::IDENTITY,
1465            scale: Vec3::ONE,
1466            playing: false,
1467            looping: true,
1468            play_time: 0.0,
1469            duration: 5.0,
1470            speed: 1.0,
1471            prewarm: false,
1472            seed: 12345,
1473            attached_to_entity: None,
1474        }
1475    }
1476
1477    pub fn play(&mut self) { self.playing = true; }
1478    pub fn stop(&mut self) { self.playing = false; self.play_time = 0.0; }
1479    pub fn pause(&mut self) { self.playing = false; }
1480
1481    pub fn update(&mut self, dt: f32) {
1482        if !self.playing { return; }
1483        self.play_time += dt * self.speed;
1484        if self.looping && self.play_time >= self.duration {
1485            self.play_time -= self.duration;
1486        } else if self.play_time >= self.duration {
1487            self.playing = false;
1488            self.play_time = self.duration;
1489        }
1490    }
1491
1492    pub fn normalized_time(&self) -> f32 {
1493        if self.duration > 0.0 { (self.play_time / self.duration).clamp(0.0, 1.0) } else { 0.0 }
1494    }
1495
1496    pub fn transform_matrix(&self) -> Mat4 {
1497        Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
1498    }
1499}
1500
1501// ---------------------------------------------------------------------------
1502// Main VFX Graph Editor
1503// ---------------------------------------------------------------------------
1504
1505#[derive(Debug)]
1506pub struct VfxGraphEditor {
1507    pub graphs: Vec<VfxGraph>,
1508    pub active_graph: usize,
1509    pub view: VfxEditorView,
1510    pub tool: VfxEditorTool,
1511    pub active_panel: VfxEditorPanel,
1512    pub selected_nodes: Vec<u64>,
1513    pub clipboard: Vec<VfxNode>,
1514    pub drag_state: VfxDragState,
1515    pub hovered_node: Option<u64>,
1516    pub search_query: String,
1517    pub show_block_library: bool,
1518    pub blackboard: VfxBlackboard,
1519    pub profiler: VfxProfiler,
1520    pub instances: Vec<VfxInstance>,
1521    pub preview_playing: bool,
1522    pub preview_time: f32,
1523    pub show_grid: bool,
1524    pub snap_to_grid: bool,
1525    pub history: Vec<Vec<VfxNode>>,
1526    pub history_pos: usize,
1527    pub category_filter: Option<String>,
1528    pub show_warnings: bool,
1529}
1530
1531impl Default for VfxGraphEditor {
1532    fn default() -> Self {
1533        let fire = VfxGraph::build_fire_effect();
1534        let smoke = VfxGraph::build_smoke_effect();
1535        let sparks = VfxGraph::build_sparks_effect();
1536
1537        let mut blackboard = VfxBlackboard::default();
1538        blackboard.add("DeltaTime", VfxValue::Float(0.016));
1539        blackboard.add("ElapsedTime", VfxValue::Float(0.0));
1540        blackboard.add("Gravity", VfxValue::Float3(Vec3::new(0.0, -9.81, 0.0)));
1541
1542        let instances = vec![
1543            VfxInstance::new("Fire"),
1544            VfxInstance::new("Smoke"),
1545            VfxInstance::new("Sparks"),
1546        ];
1547
1548        Self {
1549            graphs: vec![fire, smoke, sparks],
1550            active_graph: 0,
1551            view: VfxEditorView::default(),
1552            tool: VfxEditorTool::Select,
1553            active_panel: VfxEditorPanel::Graph,
1554            selected_nodes: Vec::new(),
1555            clipboard: Vec::new(),
1556            drag_state: VfxDragState::None,
1557            hovered_node: None,
1558            search_query: String::new(),
1559            show_block_library: false,
1560            blackboard,
1561            profiler: VfxProfiler::default(),
1562            instances,
1563            preview_playing: false,
1564            preview_time: 0.0,
1565            show_grid: true,
1566            snap_to_grid: true,
1567            history: Vec::new(),
1568            history_pos: 0,
1569            category_filter: None,
1570            show_warnings: true,
1571        }
1572    }
1573}
1574
1575impl VfxGraphEditor {
1576    pub fn active_graph(&self) -> &VfxGraph {
1577        &self.graphs[self.active_graph]
1578    }
1579
1580    pub fn active_graph_mut(&mut self) -> &mut VfxGraph {
1581        &mut self.graphs[self.active_graph]
1582    }
1583
1584    pub fn snapshot(&mut self) {
1585        let nodes = self.active_graph().nodes.clone();
1586        self.history.truncate(self.history_pos);
1587        self.history.push(nodes);
1588        self.history_pos = self.history.len();
1589    }
1590
1591    pub fn undo(&mut self) {
1592        if self.history_pos > 1 {
1593            self.history_pos -= 1;
1594            let nodes = self.history[self.history_pos - 1].clone();
1595            self.active_graph_mut().nodes = nodes;
1596        }
1597    }
1598
1599    pub fn redo(&mut self) {
1600        if self.history_pos < self.history.len() {
1601            let nodes = self.history[self.history_pos].clone();
1602            self.active_graph_mut().nodes = nodes;
1603            self.history_pos += 1;
1604        }
1605    }
1606
1607    pub fn select_node(&mut self, id: u64) {
1608        self.selected_nodes = vec![id];
1609    }
1610
1611    pub fn add_to_selection(&mut self, id: u64) {
1612        if !self.selected_nodes.contains(&id) {
1613            self.selected_nodes.push(id);
1614        }
1615    }
1616
1617    pub fn deselect_all(&mut self) {
1618        self.selected_nodes.clear();
1619    }
1620
1621    pub fn delete_selected(&mut self) {
1622        self.snapshot();
1623        let ids = self.selected_nodes.clone();
1624        for id in ids {
1625            self.active_graph_mut().remove_node(id);
1626        }
1627        self.selected_nodes.clear();
1628    }
1629
1630    pub fn copy_selected(&mut self) {
1631        let ids: std::collections::HashSet<u64> = self.selected_nodes.iter().copied().collect();
1632        self.clipboard = self.active_graph().nodes.iter()
1633            .filter(|n| ids.contains(&n.id))
1634            .cloned()
1635            .collect();
1636    }
1637
1638    pub fn paste(&mut self) {
1639        self.snapshot();
1640        let nodes_to_paste: Vec<VfxNode> = self.clipboard.clone();
1641        let graph = self.active_graph_mut();
1642        let mut new_ids = Vec::new();
1643        for mut node in nodes_to_paste {
1644            node.id = next_vfx_node_id();
1645            node.position += Vec2::new(20.0, 20.0);
1646            let id = node.id;
1647            graph.nodes.push(node);
1648            new_ids.push(id);
1649        }
1650        self.selected_nodes = new_ids;
1651    }
1652
1653    pub fn duplicate_selected(&mut self) {
1654        self.copy_selected();
1655        self.paste();
1656    }
1657
1658    pub fn add_node_at(&mut self, kind: VfxNodeKind, pos: Vec2) -> u64 {
1659        self.snapshot();
1660        let graph_pos = if self.snap_to_grid {
1661            self.view.snap_to_grid(pos)
1662        } else {
1663            pos
1664        };
1665        let node = VfxNode::new(kind, graph_pos);
1666        self.active_graph_mut().add_node(node)
1667    }
1668
1669    pub fn hit_test(&self, screen_pos: Vec2) -> Option<u64> {
1670        let graph_pos = self.view.screen_to_graph(screen_pos);
1671        // Iterate in reverse to test top-most nodes first
1672        for node in self.active_graph().nodes.iter().rev() {
1673            if node.contains_point(graph_pos) {
1674                return Some(node.id);
1675            }
1676        }
1677        None
1678    }
1679
1680    pub fn begin_drag_node(&mut self, node_id: u64, screen_pos: Vec2) {
1681        let graph_pos = self.view.screen_to_graph(screen_pos);
1682        if let Some(node) = self.active_graph().find_node(node_id) {
1683            let offset = node.position - graph_pos;
1684            self.drag_state = VfxDragState::DraggingNode { node_id, offset };
1685        }
1686    }
1687
1688    pub fn update_drag(&mut self, screen_pos: Vec2) {
1689        let graph_pos = self.view.screen_to_graph(screen_pos);
1690        match self.drag_state.clone() {
1691            VfxDragState::DraggingNode { node_id, offset } => {
1692                let new_pos = if self.snap_to_grid {
1693                    self.view.snap_to_grid(graph_pos + offset)
1694                } else {
1695                    graph_pos + offset
1696                };
1697                if let Some(node) = self.active_graph_mut().find_node_mut(node_id) {
1698                    node.position = new_pos;
1699                }
1700            }
1701            VfxDragState::Panning { last } => {
1702                self.view.pan += screen_pos - last;
1703                self.drag_state = VfxDragState::Panning { last: screen_pos };
1704            }
1705            VfxDragState::BoxSelect { start, .. } => {
1706                self.drag_state = VfxDragState::BoxSelect { start, current: graph_pos };
1707            }
1708            VfxDragState::DraggingConnection { from_node, from_port, is_input, .. } => {
1709                self.drag_state = VfxDragState::DraggingConnection {
1710                    from_node, from_port, is_input, current_pos: screen_pos,
1711                };
1712            }
1713            _ => {}
1714        }
1715    }
1716
1717    pub fn end_drag(&mut self) {
1718        if let VfxDragState::BoxSelect { start, current } = &self.drag_state.clone() {
1719            let min = Vec2::new(start.x.min(current.x), start.y.min(current.y));
1720            let max = Vec2::new(start.x.max(current.x), start.y.max(current.y));
1721            let newly_selected: Vec<u64> = self.active_graph().nodes.iter()
1722                .filter(|n| {
1723                    let cx = n.position.x + n.size.x * 0.5;
1724                    let cy = n.position.y + n.size.y * 0.5;
1725                    cx >= min.x && cx <= max.x && cy >= min.y && cy <= max.y
1726                })
1727                .map(|n| n.id)
1728                .collect();
1729            self.selected_nodes = newly_selected;
1730        }
1731        self.drag_state = VfxDragState::None;
1732    }
1733
1734    pub fn zoom(&mut self, screen_focus: Vec2, delta: f32) {
1735        self.view.zoom_at(screen_focus, delta);
1736    }
1737
1738    pub fn frame_all(&mut self, viewport: Vec2) {
1739        let nodes = self.active_graph().nodes.clone();
1740        self.view.fit_to_graph(&nodes, viewport);
1741    }
1742
1743    pub fn search_blocks(&self, query: &str) -> Vec<VfxNodeKind> {
1744        let q = query.to_lowercase();
1745        let all: Vec<VfxNodeKind> = vec![
1746            VfxNodeKind::ConstantRate, VfxNodeKind::BurstSpawn, VfxNodeKind::SetLifetime,
1747            VfxNodeKind::SetSize, VfxNodeKind::SetColor, VfxNodeKind::SetVelocityRandom,
1748            VfxNodeKind::Gravity, VfxNodeKind::Drag, VfxNodeKind::Turbulence,
1749            VfxNodeKind::EulerIntegration, VfxNodeKind::ColorOverLife, VfxNodeKind::AlphaOverLife,
1750            VfxNodeKind::ScaleOverLife, VfxNodeKind::Add, VfxNodeKind::Multiply,
1751            VfxNodeKind::Lerp, VfxNodeKind::Clamp, VfxNodeKind::Noise3D, VfxNodeKind::CurlNoise,
1752            VfxNodeKind::GetBuiltin, VfxNodeKind::SetAttribute, VfxNodeKind::Branch,
1753            VfxNodeKind::SampleGradient, VfxNodeKind::SampleCurve, VfxNodeKind::CustomHlsl,
1754        ];
1755        if q.is_empty() {
1756            all
1757        } else {
1758            all.into_iter().filter(|k| {
1759                k.label().to_lowercase().contains(&q) || k.category().to_lowercase().contains(&q)
1760            }).collect()
1761        }
1762    }
1763
1764    pub fn update_preview(&mut self, dt: f32) {
1765        if self.preview_playing {
1766            self.preview_time += dt;
1767            for inst in &mut self.instances {
1768                inst.update(dt);
1769            }
1770            // Simulate profiler data
1771            let sample = VfxProfilerSample {
1772                graph_name: self.active_graph().name.clone(),
1773                spawn_ms: 0.05 + (self.preview_time.sin() * 0.01).abs(),
1774                init_ms: 0.02,
1775                update_ms: 0.4 + (self.preview_time.cos() * 0.05).abs(),
1776                render_ms: 0.3,
1777                particle_count: self.active_graph().particle_capacity,
1778                alive_count: (self.active_graph().particle_capacity as f32 * 0.7) as u32,
1779                memory_bytes: self.active_graph().estimate_memory_bytes(),
1780                draw_calls: 2,
1781            };
1782            self.profiler.push_sample(sample);
1783        }
1784    }
1785
1786    pub fn validate_active(&mut self) -> Vec<String> {
1787        self.active_graph_mut().validate()
1788    }
1789
1790    pub fn generate_hlsl_for_active(&self) -> String {
1791        self.active_graph().generate_hlsl()
1792    }
1793}