• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

djeedai / bevy_hanabi / 21565578469

01 Feb 2026 03:38PM UTC coverage: 58.351% (-8.1%) from 66.442%
21565578469

push

github

web-flow
Update to Bevy v0.18 (#521)

Thanks to @morgenthum for the original work.

93 of 170 new or added lines in 6 files covered. (54.71%)

968 existing lines in 17 files now uncovered.

4954 of 8490 relevant lines covered (58.35%)

190.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.67
/src/graph/node.rs
1
//! Node API.
2
//!
3
//! ⚠️ This API is under construction and is missing some features. ⚠️
4
//!
5
//! The Node API is designed for effect editing, and the creation of UI tools.
6
//! It builds on top of the [Expression API] and is entirely optional. It
7
//! defines a [`Graph`] composed of [`Node`]s. Each node represent either a
8
//! [`Modifier`] or an [`Expr`]. A node has some [`Slot`]s associated with it,
9
//! representing its inputs and outputs, if any. An _output_ slot of a node can
10
//! be linked to an _input_ slot of another node to express that the value
11
//! produced by the upstream node must flow through to the input of the
12
//! downstream node.
13
//!
14
//! An effect [`Graph`] can be serialized as is, to retain its editing
15
//! capabilities. Alternatively, once the user has finished building an effect,
16
//! it can be converted to a runtime [`EffectAsset`] for use as a
17
//! [`ParticleEffect`].
18
//!
19
//! [Expression API]: crate::graph::expr
20
//! [`Modifier`]: crate::Modifier
21
//! [`Expr`]: crate::graph::expr::Expr
22
//! [`EffectAsset`]: crate::EffectAsset
23
//! [`ParticleEffect`]: crate::ParticleEffect
24

25
use std::num::NonZeroU32;
26

27
use crate::{Attribute, BuiltInOperator, ExprError, ExprHandle, Module, ValueType};
28

29
/// Identifier of a node in a graph.
30
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31
pub struct NodeId(NonZeroU32);
32

33
impl NodeId {
34
    /// Create a new node identifier.
35
    pub fn new(id: NonZeroU32) -> Self {
5✔
36
        Self(id)
5✔
37
    }
38

39
    /// Get the one-based node index.
40
    pub fn id(&self) -> NonZeroU32 {
×
41
        self.0
×
42
    }
43

44
    /// Get the zero-based index of the node in the underlying graph node array.
45
    pub fn index(&self) -> usize {
×
46
        (self.0.get() - 1) as usize
×
47
    }
48
}
49

50
/// Identifier of a slot in a graph.
51
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52
pub struct SlotId(NonZeroU32);
53

54
impl SlotId {
55
    /// Create a new slot identifier.
56
    pub fn new(id: NonZeroU32) -> Self {
10✔
57
        Self(id)
10✔
58
    }
59

60
    /// Get the one-based slot index.
61
    pub fn id(&self) -> NonZeroU32 {
×
62
        self.0
×
63
    }
64

65
    /// Get the zero-based index of the slot in the underlying graph slot array.
66
    pub fn index(&self) -> usize {
8✔
67
        (self.0.get() - 1) as usize
8✔
68
    }
69
}
70

71
/// Node slot direction.
72
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73
pub enum SlotDir {
74
    /// Input slot receiving data from outside the node.
75
    Input,
76
    /// Output slot providing data generated by the node.
77
    Output,
78
}
79

80
/// Definition of a slot of a node.
81
#[derive(Debug, Clone)]
82
pub struct SlotDef {
83
    /// Slot name.
84
    name: String,
85
    /// Slot direaction.
86
    dir: SlotDir,
87
    /// Type of values accepted by the slot. This may be `None` for variant
88
    /// slots, if the type depends on the inputs of the node during evaluation.
89
    value_type: Option<ValueType>,
90
}
91

92
impl SlotDef {
93
    /// Create a new input slot.
94
    pub fn input(name: impl Into<String>, value_type: Option<ValueType>) -> Self {
12✔
95
        Self {
96
            name: name.into(),
36✔
97
            dir: SlotDir::Input,
98
            value_type,
99
        }
100
    }
101

102
    /// Create a new output slot.
103
    pub fn output(name: impl Into<String>, value_type: Option<ValueType>) -> Self {
15✔
104
        Self {
105
            name: name.into(),
45✔
106
            dir: SlotDir::Output,
107
            value_type,
108
        }
109
    }
110

111
    /// Get the slot name.
112
    #[inline]
113
    pub fn name(&self) -> &str {
2✔
114
        &self.name
2✔
115
    }
116

117
    /// Get the slot direction.
118
    #[inline]
119
    pub fn dir(&self) -> SlotDir {
35✔
120
        self.dir
35✔
121
    }
122

123
    /// Is the slot an input slot?
124
    #[inline]
125
    pub fn is_input(&self) -> bool {
×
126
        self.dir == SlotDir::Input
×
127
    }
128

129
    /// Is the slot an input slot?
130
    #[inline]
131
    pub fn is_output(&self) -> bool {
×
132
        self.dir == SlotDir::Output
×
133
    }
134

135
    /// Get the slot value type.
136
    #[inline]
137
    pub fn value_type(&self) -> Option<ValueType> {
×
138
        self.value_type
×
139
    }
140
}
141

142
/// Single slot of a node.
143
#[derive(Debug, Clone)]
144
pub struct Slot {
145
    /// Owner node identifier.
146
    node_id: NodeId,
147
    /// Identifier.
148
    id: SlotId,
149
    /// Slot definition.
150
    def: SlotDef,
151
    /// Linked slots.
152
    linked_slots: Vec<SlotId>,
153
}
154

155
impl Slot {
156
    /// Create a new slot.
157
    pub fn new(node_id: NodeId, slot_id: SlotId, slot_def: SlotDef) -> Self {
10✔
158
        Slot {
159
            node_id,
160
            id: slot_id,
161
            def: slot_def,
162
            linked_slots: vec![],
10✔
163
        }
164
    }
165

166
    /// Get the node identifier of the node this slot is from.
167
    pub fn node_id(&self) -> NodeId {
62✔
168
        self.node_id
62✔
169
    }
170

171
    /// Get the slot identifier.
172
    pub fn id(&self) -> SlotId {
11✔
173
        self.id
11✔
174
    }
175

176
    /// Get the slot definition.
177
    pub fn def(&self) -> &SlotDef {
2✔
178
        &self.def
2✔
179
    }
180

181
    /// Get the slot direction.
182
    pub fn dir(&self) -> SlotDir {
35✔
183
        self.def.dir()
70✔
184
    }
185

186
    /// Check if this slot is an input slot.
187
    ///
188
    /// This is a convenience helper for `self.dir() == SlotDir::Input`.
189
    pub fn is_input(&self) -> bool {
20✔
190
        self.dir() == SlotDir::Input
20✔
191
    }
192

193
    /// Check if this slot is an output slot.
194
    ///
195
    /// This is a convenience helper for `self.dir() == SlotDir::Output`.
196
    pub fn is_output(&self) -> bool {
15✔
197
        self.dir() == SlotDir::Output
15✔
198
    }
199

200
    /// Link this output slot to an input slot.
201
    ///
202
    /// # Panics
203
    ///
204
    /// Panics if this slot's direction is `SlotDir::Input`.
205
    fn link_to(&mut self, input: SlotId) {
4✔
206
        assert!(self.is_output());
12✔
207
        if !self.linked_slots.contains(&input) {
12✔
208
            self.linked_slots.push(input);
4✔
209
        }
210
    }
211

212
    fn unlink_from(&mut self, input: SlotId) -> bool {
×
213
        assert!(self.is_output());
×
214
        if let Some(index) = self.linked_slots.iter().position(|&s| s == input) {
×
UNCOV
215
            self.linked_slots.remove(index);
×
UNCOV
216
            true
×
217
        } else {
218
            false
×
219
        }
220
    }
221

222
    fn link_input(&mut self, output: SlotId) {
4✔
223
        assert!(self.is_input());
12✔
224
        if self.linked_slots.is_empty() {
12✔
225
            self.linked_slots.push(output);
8✔
226
        } else {
227
            self.linked_slots[0] = output;
×
228
        }
229
    }
230

231
    fn unlink_input(&mut self) {
×
232
        assert!(self.is_input());
×
233
        self.linked_slots.clear();
×
234
    }
235
}
236

237
/// Effect graph.
238
///
239
/// An effect graph represents an editable version of an [`EffectAsset`]. The
240
/// graph is composed of [`Node`]s, which represent either a [`Modifier`] or an
241
/// expression [`Expr`]. Expression nodes are linked together to form more
242
/// complex expressions which are then assigned to the modifier inputs. Once the
243
/// graph is ready, it can be converted into an [`EffectAsset`].
244
///
245
/// [`EffectAsset`]: crate::EffectAsset
246
/// [`Modifier`]: crate::Modifier
247
/// [`Expr`]: crate::graph::Expr
248
#[derive(Default)]
249
pub struct Graph {
250
    nodes: Vec<Box<dyn Node>>,
251
    slots: Vec<Slot>,
252
}
253

254
impl std::fmt::Debug for Graph {
255
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
256
        f.debug_struct("Graph").field("slots", &self.slots).finish()
×
257
    }
258
}
259

260
impl Graph {
261
    /// Create a new empty graph.
262
    ///
263
    /// An empty graph doesn't represent a valid [`EffectAsset`]. You must add
264
    /// some [`Node`]s with [`add_node()`] and [`link()`] them together to form
265
    /// a valid graph.
266
    ///
267
    /// [`EffectAsset`]: crate::EffectAsset
268
    /// [`add_node()`]: crate::graph::Graph::add_node
269
    /// [`link()`]: crate::graph::Graph::link
270
    pub fn new() -> Self {
1✔
271
        Self::default()
1✔
272
    }
273

274
    /// Add a node to the graph, without any link.
275
    ///
276
    /// # Example
277
    ///
278
    /// ```
279
    /// # use bevy_hanabi::*;
280
    /// let mut graph = Graph::new();
281
    /// let time_node = graph.add_node(TimeNode::default());
282
    /// ```
283
    #[inline]
284
    pub fn add_node<N>(&mut self, node: N) -> NodeId
5✔
285
    where
286
        N: Node + 'static,
287
    {
288
        self.add_node_impl(Box::new(node))
15✔
289
    }
290

291
    fn add_node_impl(&mut self, node: Box<dyn Node>) -> NodeId {
5✔
292
        let index = self.nodes.len() as u32;
10✔
293
        let node_id = NodeId::new(NonZeroU32::new(index + 1).unwrap());
25✔
294

295
        for slot_def in node.slots() {
20✔
296
            let slot_id = SlotId::new(NonZeroU32::new(self.slots.len() as u32 + 1).unwrap());
297
            let slot = Slot::new(node_id, slot_id, slot_def.clone());
298
            self.slots.push(slot);
299
        }
300

301
        self.nodes.push(node);
15✔
302

303
        node_id
5✔
304
    }
305

306
    /// Link an output slot of a node to an input slot of another node.
307
    ///
308
    /// # Panics
309
    ///
310
    /// Panics if the `output` argument doesn't reference an output slot of an
311
    /// existing node, or the `input` argument doesn't reference an input slot
312
    /// of an existing node.
313
    pub fn link(&mut self, output: SlotId, input: SlotId) {
4✔
314
        let out_slot = self.get_slot_mut(output);
16✔
315
        assert!(out_slot.is_output());
12✔
316
        out_slot.link_to(input);
12✔
317

318
        let in_slot = self.get_slot_mut(input);
16✔
319
        assert!(in_slot.is_input());
12✔
320
        in_slot.link_input(output);
12✔
321
    }
322

323
    /// Unlink an output slot of a node from an input slot of another node.
324
    ///
325
    /// # Panics
326
    ///
327
    /// Panics if the `output` argument doesn't reference an output slot of an
328
    /// existing node, or the `input` argument doesn't reference an input slot
329
    /// of an existing node.
330
    pub fn unlink(&mut self, output: SlotId, input: SlotId) {
×
331
        let out_slot = self.get_slot_mut(output);
×
332
        assert!(out_slot.is_output());
×
333
        if out_slot.unlink_from(input) {
×
334
            let in_slot = self.get_slot_mut(input);
×
335
            assert!(in_slot.is_input());
×
336
            in_slot.unlink_input();
×
337
        }
338
    }
339

340
    /// Unlink all remote slots from a given slot.
341
    pub fn unlink_all(&mut self, slot_id: SlotId) {
×
342
        let slot = self.get_slot_mut(slot_id);
×
343
        let linked_slots = std::mem::take(&mut slot.linked_slots);
×
344
        for remote_id in &linked_slots {
×
UNCOV
345
            let remote_slot = self.get_slot_mut(*remote_id);
×
346
            if remote_slot.is_input() {
×
347
                remote_slot.unlink_input();
×
348
            } else {
349
                remote_slot.unlink_from(slot_id);
×
350
            }
351
        }
352
    }
353

354
    /// Get all slots of a node.
355
    pub fn slots(&self, node_id: NodeId) -> Vec<SlotId> {
×
356
        self.slots
×
357
            .iter()
358
            .filter_map(|s| {
×
359
                if s.node_id() == node_id {
×
360
                    Some(s.id())
×
361
                } else {
362
                    None
×
363
                }
364
            })
365
            .collect()
366
    }
367

368
    /// Get a given input slot of a node by name.
369
    pub fn input_slot<'a, 'b: 'a, S: Into<&'b str>>(
×
370
        &'a self,
371
        node_id: NodeId,
372
        name: S,
373
    ) -> Option<SlotId> {
374
        let name = name.into();
×
375
        self.slots
×
376
            .iter()
377
            .find(|s| s.node_id() == node_id && s.is_input() && s.def().name() == name)
×
378
            .map(|s| s.id)
×
379
    }
380

381
    /// Get all input slots of a node.
382
    pub fn input_slots(&self, node_id: NodeId) -> Vec<SlotId> {
4✔
383
        self.slots
4✔
384
            .iter()
385
            .filter_map(|s| {
32✔
386
                if s.node_id() == node_id && s.is_input() {
52✔
387
                    Some(s.id())
8✔
388
                } else {
389
                    None
20✔
390
                }
391
            })
392
            .collect()
393
    }
394

395
    /// Get a given output slot of a node by name.
396
    pub fn output_slot<'a, 'b: 'a, S: Into<&'b str>>(
1✔
397
        &'a self,
398
        node_id: NodeId,
399
        name: S,
400
    ) -> Option<SlotId> {
401
        let name = name.into();
3✔
402
        self.slots
1✔
403
            .iter()
404
            .find(|s| s.node_id() == node_id && s.is_output() && s.def().name() == name)
17✔
405
            .map(|s| s.id)
1✔
406
    }
407

408
    /// Get all output slots of a node.
409
    pub fn output_slots(&self, node_id: NodeId) -> Vec<SlotId> {
3✔
410
        self.slots
3✔
411
            .iter()
412
            .filter_map(|s| {
27✔
413
                if s.node_id() == node_id && s.is_output() {
34✔
414
                    Some(s.id())
3✔
415
                } else {
416
                    None
21✔
417
                }
418
            })
419
            .collect()
420
    }
421

422
    /// Find a slot ID by slot name.
423
    pub fn get_slot_id<'a, 'b: 'a, S: Into<&'b str>>(&'a self, name: S) -> Option<SlotId> {
×
424
        let name = name.into();
×
425
        self.slots
×
426
            .iter()
427
            .find(|&s| s.def().name() == name)
×
428
            .map(|s| s.id)
×
429
    }
430

431
    #[allow(dead_code)] // TEMP
432
    fn get_slot(&self, id: SlotId) -> &Slot {
×
433
        let index = id.index();
×
434
        assert!(index < self.slots.len());
×
435
        &self.slots[index]
×
436
    }
437

438
    fn get_slot_mut(&mut self, id: SlotId) -> &mut Slot {
8✔
439
        let index = id.index();
24✔
440
        assert!(index < self.slots.len());
24✔
441
        &mut self.slots[index]
8✔
442
    }
443
}
444

445
/// Generic graph node.
446
pub trait Node {
447
    /// Get the list of slots of this node.
448
    ///
449
    /// The list contains both input and output slots, without any guaranteed
450
    /// order.
451
    fn slots(&self) -> &[SlotDef];
452

453
    /// Evaluate the node from the given input expressions, and optionally
454
    /// produce output expression(s).
455
    ///
456
    /// The expressions themselves are not evaluated (that is, _e.g._ "3 + 2" is
457
    /// _not_ reduced to "5").
458
    fn eval(
459
        &self,
460
        module: &mut Module,
461
        inputs: Vec<ExprHandle>,
462
    ) -> Result<Vec<ExprHandle>, ExprError>;
463
}
464

465
/// Graph node to add two values.
466
#[derive(Debug, Clone)]
467
pub struct AddNode {
468
    slots: [SlotDef; 3],
469
}
470

471
impl Default for AddNode {
472
    fn default() -> Self {
2✔
473
        Self {
474
            slots: [
2✔
475
                SlotDef::input("lhs", None),
476
                SlotDef::input("rhs", None),
477
                SlotDef::output("result", None),
478
            ],
479
        }
480
    }
481
}
482

483
impl Node for AddNode {
484
    fn slots(&self) -> &[SlotDef] {
1✔
485
        &self.slots
1✔
486
    }
487

488
    fn eval(
3✔
489
        &self,
490
        module: &mut Module,
491
        inputs: Vec<ExprHandle>,
492
    ) -> Result<Vec<ExprHandle>, ExprError> {
493
        if inputs.len() != 2 {
3✔
494
            return Err(ExprError::GraphEvalError(format!(
4✔
495
                "Unexpected input count to AddNode::eval(): expected 2, got {}",
2✔
496
                inputs.len()
2✔
497
            )));
498
        }
499
        let mut inputs = inputs.into_iter();
500
        let left = inputs.next().unwrap();
501
        let right = inputs.next().unwrap();
502
        let add = module.add(left, right);
503
        Ok(vec![add])
504
    }
505
}
506

507
/// Graph node to subtract two values.
508
#[derive(Debug, Clone)]
509
pub struct SubNode {
510
    slots: [SlotDef; 3],
511
}
512

513
impl Default for SubNode {
514
    fn default() -> Self {
1✔
515
        Self {
516
            slots: [
1✔
517
                SlotDef::input("lhs", None),
518
                SlotDef::input("rhs", None),
519
                SlotDef::output("result", None),
520
            ],
521
        }
522
    }
523
}
524

525
impl Node for SubNode {
526
    fn slots(&self) -> &[SlotDef] {
×
527
        &self.slots
×
528
    }
529

530
    fn eval(
3✔
531
        &self,
532
        module: &mut Module,
533
        inputs: Vec<ExprHandle>,
534
    ) -> Result<Vec<ExprHandle>, ExprError> {
535
        if inputs.len() != 2 {
3✔
536
            return Err(ExprError::GraphEvalError(format!(
4✔
537
                "Unexpected input count to SubNode::eval(): expected 2, got
2✔
538
{}",
2✔
539
                inputs.len()
2✔
540
            )));
541
        }
542
        let mut inputs = inputs.into_iter();
543
        let left = inputs.next().unwrap();
544
        let right = inputs.next().unwrap();
545
        let sub = module.sub(left, right);
546
        Ok(vec![sub])
547
    }
548
}
549

550
/// Graph node to multiply two values.
551
#[derive(Debug, Clone)]
552
pub struct MulNode {
553
    slots: [SlotDef; 3],
554
}
555

556
impl Default for MulNode {
557
    fn default() -> Self {
2✔
558
        Self {
559
            slots: [
2✔
560
                SlotDef::input("lhs", None),
561
                SlotDef::input("rhs", None),
562
                SlotDef::output("result", None),
563
            ],
564
        }
565
    }
566
}
567

568
impl Node for MulNode {
569
    fn slots(&self) -> &[SlotDef] {
1✔
570
        &self.slots
1✔
571
    }
572

573
    fn eval(
3✔
574
        &self,
575
        module: &mut Module,
576
        inputs: Vec<ExprHandle>,
577
    ) -> Result<Vec<ExprHandle>, ExprError> {
578
        if inputs.len() != 2 {
3✔
579
            return Err(ExprError::GraphEvalError(format!(
4✔
580
                "Unexpected input count to MulNode::eval(): expected 2, got
2✔
581
{}",
2✔
582
                inputs.len()
2✔
583
            )));
584
        }
585
        let mut inputs = inputs.into_iter();
586
        let left = inputs.next().unwrap();
587
        let right = inputs.next().unwrap();
588
        let mul = module.mul(left, right);
589
        Ok(vec![mul])
590
    }
591
}
592

593
/// Graph node to divide two values.
594
#[derive(Debug, Clone)]
595
pub struct DivNode {
596
    slots: [SlotDef; 3],
597
}
598

599
impl Default for DivNode {
600
    fn default() -> Self {
1✔
601
        Self {
602
            slots: [
1✔
603
                SlotDef::input("lhs", None),
604
                SlotDef::input("rhs", None),
605
                SlotDef::output("result", None),
606
            ],
607
        }
608
    }
609
}
610

611
impl Node for DivNode {
612
    fn slots(&self) -> &[SlotDef] {
×
613
        &self.slots
×
614
    }
615

616
    fn eval(
3✔
617
        &self,
618
        module: &mut Module,
619
        inputs: Vec<ExprHandle>,
620
    ) -> Result<Vec<ExprHandle>, ExprError> {
621
        if inputs.len() != 2 {
3✔
622
            return Err(ExprError::GraphEvalError(format!(
4✔
623
                "Unexpected input count to DivNode::eval(): expected 2, got
2✔
624
{}",
2✔
625
                inputs.len()
2✔
626
            )));
627
        }
628
        let mut inputs = inputs.into_iter();
629
        let left = inputs.next().unwrap();
630
        let right = inputs.next().unwrap();
631
        let div = module.div(left, right);
632
        Ok(vec![div])
633
    }
634
}
635

636
/// Graph node to get any single particle attribute.
637
#[derive(Debug, Clone)]
638
pub struct AttributeNode {
639
    /// The attribute to get.
640
    attr: Attribute,
641
    /// The output slot corresponding to the get value.
642
    slots: [SlotDef; 1],
643
}
644

645
impl Default for AttributeNode {
646
    fn default() -> Self {
×
647
        Self::new(Attribute::POSITION)
×
648
    }
649
}
650

651
impl AttributeNode {
652
    /// Create a new attribute node for the given [`Attribute`].
653
    pub fn new(attr: Attribute) -> Self {
3✔
654
        Self {
655
            attr,
656
            slots: [SlotDef::output(attr.name(), Some(attr.value_type()))],
12✔
657
        }
658
    }
659
}
660

661
impl AttributeNode {
662
    /// Get the attribute this node reads.
663
    pub fn attr(&self) -> Attribute {
×
664
        self.attr
×
665
    }
666

667
    /// Set the attribute this node reads.
668
    pub fn set_attr(&mut self, attr: Attribute) {
×
669
        self.attr = attr;
×
670
    }
671
}
672

673
impl Node for AttributeNode {
674
    fn slots(&self) -> &[SlotDef] {
2✔
675
        &self.slots
2✔
676
    }
677

678
    fn eval(
2✔
679
        &self,
680
        module: &mut Module,
681
        inputs: Vec<ExprHandle>,
682
    ) -> Result<Vec<ExprHandle>, ExprError> {
683
        if !inputs.is_empty() {
2✔
684
            return Err(ExprError::GraphEvalError(
1✔
685
                "Unexpected non-empty input to
1✔
686
AttributeNode::eval()."
1✔
687
                    .to_string(),
1✔
688
            ));
689
        }
690
        let attr = module.attr(self.attr);
4✔
691
        Ok(vec![attr])
1✔
692
    }
693
}
694

695
/// Graph node to get various time values related to the effect system.
696
#[derive(Debug, Clone)]
697
pub struct TimeNode {
698
    /// Output slots corresponding to the various time-related quantities.
699
    slots: [SlotDef; 2],
700
}
701

702
impl Default for TimeNode {
703
    fn default() -> Self {
2✔
704
        Self {
705
            slots: [BuiltInOperator::Time, BuiltInOperator::DeltaTime]
2✔
706
                .map(|op| SlotDef::output(op.name(), Some(op.value_type()))),
707
        }
708
    }
709
}
710

711
impl Node for TimeNode {
712
    fn slots(&self) -> &[SlotDef] {
1✔
713
        &self.slots
1✔
714
    }
715

716
    fn eval(
2✔
717
        &self,
718
        module: &mut Module,
719
        inputs: Vec<ExprHandle>,
720
    ) -> Result<Vec<ExprHandle>, ExprError> {
721
        if !inputs.is_empty() {
2✔
722
            return Err(ExprError::GraphEvalError(
1✔
723
                "Unexpected non-empty input to
1✔
724
TimeNode::eval()."
1✔
725
                    .to_string(),
1✔
726
            ));
727
        }
728
        Ok([BuiltInOperator::Time, BuiltInOperator::DeltaTime]
1✔
729
            .map(|op| module.builtin(op))
7✔
730
            .to_vec())
1✔
731
    }
732
}
733

734
/// Graph node to normalize a vector value.
735
#[derive(Debug, Clone)]
736
pub struct NormalizeNode {
737
    /// Input and output vectors.
738
    slots: [SlotDef; 2],
739
}
740

741
impl Default for NormalizeNode {
742
    fn default() -> Self {
1✔
743
        Self {
744
            slots: [SlotDef::output("in", None), SlotDef::output("out", None)],
3✔
745
        }
746
    }
747
}
748

749
impl Node for NormalizeNode {
750
    fn slots(&self) -> &[SlotDef] {
×
751
        &self.slots
×
752
    }
753

754
    fn eval(
2✔
755
        &self,
756
        module: &mut Module,
757
        inputs: Vec<ExprHandle>,
758
    ) -> Result<Vec<ExprHandle>, ExprError> {
759
        if inputs.len() != 1 {
2✔
760
            return Err(ExprError::GraphEvalError(
1✔
761
                "Unexpected input slot count to NormalizeNode::eval() not
1✔
762
equal to one."
1✔
763
                    .to_string(),
1✔
764
            ));
765
        }
766
        let input = inputs.into_iter().next().unwrap();
767
        let norm = module.normalize(input);
768
        Ok(vec![norm])
769
    }
770
}
771

772
#[cfg(test)]
773
mod tests {
774
    use bevy::prelude::*;
775

776
    use super::*;
777
    use crate::{
778
        node::Node as _, EvalContext, ModifierContext, ParticleLayout, PropertyLayout, ShaderWriter,
779
    };
780

781
    #[test]
782
    fn add() {
783
        let node = AddNode::default();
784

785
        let mut module = Module::default();
786

787
        let ret = node.eval(&mut module, vec![]);
788
        assert!(matches!(ret, Err(ExprError::GraphEvalError(_))));
789
        let three = module.lit(3.);
790
        let ret = node.eval(&mut module, vec![three]);
791
        assert!(matches!(ret, Err(ExprError::GraphEvalError(_))));
792

793
        let two = module.lit(2.);
794
        let outputs = node.eval(&mut module, vec![three, two]).unwrap();
795
        assert_eq!(outputs.len(), 1);
796
        let out = outputs[0];
797

798
        let property_layout = PropertyLayout::default();
799
        let particle_layout = ParticleLayout::default();
800
        let mut context =
801
            ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
802
        let str = context.eval(&module, out).unwrap();
803
        assert_eq!(str, "(3.) + (2.)".to_string());
804
    }
805

806
    #[test]
807
    fn sub() {
808
        let node = SubNode::default();
809

810
        let mut module = Module::default();
811

812
        let ret = node.eval(&mut module, vec![]);
813
        assert!(matches!(ret, Err(ExprError::GraphEvalError(_))));
814
        let three = module.lit(3.);
815
        let ret = node.eval(&mut module, vec![three]);
816
        assert!(matches!(ret, Err(ExprError::GraphEvalError(_))));
817

818
        let two = module.lit(2.);
819
        let outputs = node.eval(&mut module, vec![three, two]).unwrap();
820
        assert_eq!(outputs.len(), 1);
821
        let out = outputs[0];
822
        let property_layout = PropertyLayout::default();
823
        let particle_layout = ParticleLayout::default();
824
        let mut context =
825
            ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
826
        let str = context.eval(&module, out).unwrap();
827
        assert_eq!(str, "(3.) - (2.)".to_string());
828
    }
829

830
    #[test]
831
    fn mul() {
832
        let node = MulNode::default();
833

834
        let mut module = Module::default();
835

836
        let ret = node.eval(&mut module, vec![]);
837
        assert!(matches!(ret, Err(ExprError::GraphEvalError(_))));
838
        let three = module.lit(3.);
839
        let ret = node.eval(&mut module, vec![three]);
840
        assert!(matches!(ret, Err(ExprError::GraphEvalError(_))));
841

842
        let two = module.lit(2.);
843
        let outputs = node.eval(&mut module, vec![three, two]).unwrap();
844
        assert_eq!(outputs.len(), 1);
845
        let out = outputs[0];
846
        let property_layout = PropertyLayout::default();
847
        let particle_layout = ParticleLayout::default();
848
        let mut context =
849
            ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
850
        let str = context.eval(&module, out).unwrap();
851
        assert_eq!(str, "(3.) * (2.)".to_string());
852
    }
853

854
    #[test]
855
    fn div() {
856
        let node = DivNode::default();
857

858
        let mut module = Module::default();
859

860
        let ret = node.eval(&mut module, vec![]);
861
        assert!(matches!(ret, Err(ExprError::GraphEvalError(_))));
862
        let three = module.lit(3.);
863
        let ret = node.eval(&mut module, vec![three]);
864
        assert!(matches!(ret, Err(ExprError::GraphEvalError(_))));
865

866
        let two = module.lit(2.);
867
        let outputs = node.eval(&mut module, vec![three, two]).unwrap();
868
        assert_eq!(outputs.len(), 1);
869
        let out = outputs[0];
870
        let property_layout = PropertyLayout::default();
871
        let particle_layout = ParticleLayout::default();
872
        let mut context =
873
            ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
874
        let str = context.eval(&module, out).unwrap();
875
        assert_eq!(str, "(3.) / (2.)".to_string());
876
    }
877

878
    #[test]
879
    fn attr() {
880
        let node = AttributeNode::new(Attribute::POSITION);
881

882
        let mut module = Module::default();
883

884
        let three = module.lit(3.);
885
        let ret = node.eval(&mut module, vec![three]);
886
        assert!(matches!(ret, Err(ExprError::GraphEvalError(_))));
887

888
        let outputs = node.eval(&mut module, vec![]).unwrap();
889
        assert_eq!(outputs.len(), 1);
890
        let out = outputs[0];
891
        let property_layout = PropertyLayout::default();
892
        let particle_layout = ParticleLayout::default();
893
        let mut context =
894
            ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
895
        let str = context.eval(&module, out).unwrap();
896
        assert_eq!(str, format!("particle.{}", Attribute::POSITION.name()));
897
    }
898

899
    #[test]
900
    fn time() {
901
        let node = TimeNode::default();
902

903
        let mut module = Module::default();
904

905
        let three = module.lit(3.);
906
        let ret = node.eval(&mut module, vec![three]);
907
        assert!(matches!(ret, Err(ExprError::GraphEvalError(_))));
908

909
        let outputs = node.eval(&mut module, vec![]).unwrap();
910
        assert_eq!(outputs.len(), 2);
911
        let property_layout = PropertyLayout::default();
912
        let particle_layout = ParticleLayout::default();
913
        let mut context =
914
            ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
915
        let str0 = context.eval(&module, outputs[0]).unwrap();
916
        let str1 = context.eval(&module, outputs[1]).unwrap();
917
        assert_eq!(str0, format!("sim_params.{}", BuiltInOperator::Time.name()));
918
        assert_eq!(
919
            str1,
920
            format!("sim_params.{}", BuiltInOperator::DeltaTime.name())
921
        );
922
    }
923

924
    #[test]
925
    fn normalize() {
926
        let node = NormalizeNode::default();
927

928
        let mut module = Module::default();
929

930
        let ret = node.eval(&mut module, vec![]);
931
        assert!(matches!(ret, Err(ExprError::GraphEvalError(_))));
932

933
        let ones = module.lit(Vec3::ONE);
934
        let outputs = node.eval(&mut module, vec![ones]).unwrap();
935
        assert_eq!(outputs.len(), 1);
936
        let property_layout = PropertyLayout::default();
937
        let particle_layout = ParticleLayout::default();
938
        let mut context =
939
            ShaderWriter::new(ModifierContext::Update, &property_layout, &particle_layout);
940
        let str = context.eval(&module, outputs[0]).unwrap();
941
        assert_eq!(str, "normalize(vec3<f32>(1.,1.,1.))".to_string());
942
    }
943

944
    #[test]
945
    fn graph() {
946
        let mut g = Graph::new();
947

948
        let nid_pos = g.add_node(AttributeNode::new(Attribute::POSITION));
949
        let nid_add = g.add_node(AddNode::default());
950
        let sid_pos = g.output_slots(nid_pos)[0];
951
        let sid_add_lhs = g.input_slots(nid_add)[0];
952
        let sid_add_rhs = g.input_slots(nid_add)[1];
953
        g.link(sid_pos, sid_add_lhs);
954

955
        let nid_vel = g.add_node(AttributeNode::new(Attribute::VELOCITY));
956
        let nid_mul = g.add_node(MulNode::default());
957
        let nid_dt = g.add_node(TimeNode::default());
958
        let sid_vel = g.output_slots(nid_vel)[0];
959
        let sid_dt = g
960
            .output_slot(nid_dt, BuiltInOperator::DeltaTime.name())
961
            .unwrap();
962
        let sid_mul_lhs = g.input_slots(nid_mul)[0];
963
        let sid_mul_rhs = g.input_slots(nid_mul)[1];
964
        g.link(sid_vel, sid_mul_lhs);
965
        g.link(sid_dt, sid_mul_rhs);
966

967
        let sid_mul_out = g.output_slots(nid_mul)[0];
968
        g.link(sid_mul_out, sid_add_rhs);
969
    }
970
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc