use std::{collections::BTreeMap, num::NonZeroU32, sync::Arc};
use bevy::{
asset::{Asset, AssetPath},
math::{UVec2, Vec2, Vec3, Vec4},
reflect::TypePath,
};
use bevy_hanabi::{
Attribute, BuiltInOperator, CpuValue, Gradient, ScalarType, SimulationCondition,
SimulationSpace, SpawnerSettings, Value, ValueType, VectorType,
graph::expr::{BinaryOperator, TernaryOperator, UnaryOperator},
};
use serde::{Deserialize, Serialize};
use crate::ModifierGroup;
pub type SharedStr = Arc<str>;
pub const FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct NodeId(pub NonZeroU32);
impl NodeId {
pub fn new(one_based: u32) -> Option<Self> {
NonZeroU32::new(one_based).map(Self)
}
pub fn get(&self) -> u32 {
self.0.get()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct StackId(pub NonZeroU32);
impl StackId {
pub fn new(one_based: u32) -> Option<Self> {
NonZeroU32::new(one_based).map(Self)
}
pub fn get(&self) -> u32 {
self.0.get()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct PropertyId(pub NonZeroU32);
impl PropertyId {
pub fn new(one_based: u32) -> Option<Self> {
NonZeroU32::new(one_based).map(Self)
}
pub fn get(&self) -> u32 {
self.0.get()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct SlotId(pub NonZeroU32);
impl SlotId {
pub fn new(one_based: u32) -> Option<Self> {
NonZeroU32::new(one_based).map(Self)
}
pub fn get(&self) -> u32 {
self.0.get()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ExprNode {
Literal(Value),
Property(PropertyId),
Attribute(Attribute),
ParentAttribute(Attribute),
BuiltIn(BuiltInOperator),
Unary(UnaryOperator),
Binary(BinaryOperator),
Ternary(TernaryOperator),
Cast(ValueType),
Image(ImageBinding),
TextureSample,
SelectImage { count: u32 },
}
pub const MAX_SELECT_IMAGE_INPUTS: usize = 16;
const SELECT_IMAGE_PORTS: [&str; MAX_SELECT_IMAGE_INPUTS + 1] = [
"index", "image0", "image1", "image2", "image3", "image4", "image5", "image6", "image7",
"image8", "image9", "image10", "image11", "image12", "image13", "image14", "image15",
];
pub fn is_select_image_input(port: &str) -> bool {
port != "index"
}
impl ExprNode {
pub fn input_ports(&self) -> &'static [&'static str] {
match self {
ExprNode::Unary(_) | ExprNode::Cast(_) => &["in"],
ExprNode::Binary(_) => &["lhs", "rhs"],
ExprNode::Ternary(_) => &["a", "b", "c"],
ExprNode::TextureSample => &["image", "coordinates"],
ExprNode::SelectImage { count } => {
let n = (*count as usize).min(MAX_SELECT_IMAGE_INPUTS);
&SELECT_IMAGE_PORTS[..=n]
}
ExprNode::Literal(_)
| ExprNode::Property(_)
| ExprNode::Attribute(_)
| ExprNode::ParentAttribute(_)
| ExprNode::BuiltIn(_)
| ExprNode::Image(_) => &[],
}
}
pub fn has_image_input(&self) -> bool {
matches!(self, ExprNode::TextureSample | ExprNode::SelectImage { .. })
}
pub fn port_is_image(&self, port: &str) -> bool {
match self {
ExprNode::TextureSample => port == "image",
ExprNode::SelectImage { .. } => is_select_image_input(port),
_ => false,
}
}
pub fn operand_default(&self, port: &str) -> InputDefault {
use BinaryOperator as B;
use UnaryOperator as U;
if matches!(self, ExprNode::TextureSample) && port == "image" {
return ImageBinding::Unbound.into();
}
let value = match (self, port) {
(ExprNode::TextureSample, "coordinates") => Value::from(Vec2::ZERO),
(ExprNode::SelectImage { .. }, "index") => Value::from(0u32),
(ExprNode::Binary(B::Cross | B::Dot), _)
| (ExprNode::Binary(B::Vec4XyzW), "lhs")
| (ExprNode::Unary(U::Normalize | U::Z), _) => Value::from(Vec3::ZERO),
(ExprNode::Unary(U::X | U::Y), _) => Value::from(Vec2::ZERO),
(ExprNode::Unary(U::W | U::Pack4x8snorm | U::Pack4x8unorm), _) => {
Value::from(Vec4::ZERO)
}
(ExprNode::Unary(U::Unpack4x8snorm | U::Unpack4x8unorm), _) => Value::from(0u32),
(ExprNode::Unary(U::All | U::Any), _) => Value::from(false),
_ => Value::from(0.0f32),
};
value.into()
}
pub fn operands_share_type(&self) -> bool {
use BinaryOperator as B;
use TernaryOperator as T;
matches!(
self,
ExprNode::Binary(
B::Add
| B::Sub
| B::Min
| B::Max
| B::Remainder
| B::Step
| B::Atan2
| B::Distance
| B::UniformRand
| B::NormalRand
| B::GreaterThan
| B::GreaterThanOrEqual
| B::LessThan
| B::LessThanOrEqual,
) | ExprNode::Ternary(T::Mix | T::Clamp | T::SmoothStep)
)
}
pub fn output_value_type(
&self,
mut operand: impl FnMut(&str) -> Option<ValueType>,
) -> Option<ValueType> {
use BinaryOperator as B;
use UnaryOperator as U;
let f32t = ValueType::Scalar(ScalarType::Float);
let boolt = ValueType::Scalar(ScalarType::Bool);
let first = self.input_ports().first().copied().and_then(&mut operand);
Some(match self {
ExprNode::Binary(B::Dot | B::Distance) | ExprNode::Unary(U::Length) => f32t,
ExprNode::Unary(U::All | U::Any) => boolt,
ExprNode::Unary(U::Pack4x8snorm | U::Pack4x8unorm) => {
ValueType::Scalar(ScalarType::Uint)
}
ExprNode::Unary(U::Unpack4x8snorm | U::Unpack4x8unorm) => {
ValueType::Vector(VectorType::VEC4F)
}
ExprNode::Binary(B::Vec2) => ValueType::Vector(VectorType::VEC2F),
ExprNode::Ternary(TernaryOperator::Vec3) => ValueType::Vector(VectorType::VEC3F),
ExprNode::Binary(B::Vec4XyzW) => ValueType::Vector(VectorType::VEC4F),
ExprNode::Binary(B::Cross) => ValueType::Vector(VectorType::VEC3F),
ExprNode::Unary(U::X | U::Y | U::Z | U::W) => match first? {
ValueType::Vector(v) => ValueType::Scalar(v.elem_type()),
scalar => scalar,
},
ExprNode::Binary(
B::GreaterThan | B::GreaterThanOrEqual | B::LessThan | B::LessThanOrEqual,
) => match first? {
ValueType::Vector(v) => {
ValueType::Vector(VectorType::new(ScalarType::Bool, v.count() as u8))
}
ValueType::Scalar(_) => boolt,
other => other,
},
_ => return first,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TextureValue {
Asset(AssetPath<'static>),
Slot { name: SharedStr },
}
impl Default for TextureValue {
fn default() -> Self {
TextureValue::Slot {
name: SharedStr::from("texture"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ImageBinding {
#[default]
Unbound,
Asset(AssetPath<'static>),
Slot(SlotId),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TextureSlotDef {
pub id: SlotId,
pub name: SharedStr,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum GradientVec3 {
Analytical(Gradient<Vec3>),
Lut(TextureValue),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum GradientVec4 {
Analytical(Gradient<Vec4>),
Lut(TextureValue),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EditValue {
Bool(bool),
U32(u32),
Scalar(Value),
UVec2(UVec2),
Color(Vec4),
Attribute(Attribute),
CpuVec3(CpuValue<Vec3>),
CpuVec4(CpuValue<Vec4>),
Gradient3(GradientVec3),
Gradient4(GradientVec4),
Texture(TextureValue),
Enum {
type_path: SharedStr,
variant: SharedStr,
},
Flags {
type_path: SharedStr,
bits: u64,
},
Raw(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ModifierNodeData {
Known {
type_path: SharedStr,
config: BTreeMap<SharedStr, EditValue>,
},
Unknown {
type_path: SharedStr,
raw: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum NodePayload {
Expr(ExprNode),
Modifier(ModifierNodeData),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum InputDefault {
Value(Value),
Image(ImageBinding),
}
impl InputDefault {
pub fn as_value(&self) -> Option<Value> {
match self {
InputDefault::Value(v) => Some(*v),
InputDefault::Image(_) => None,
}
}
pub fn as_image(&self) -> Option<&ImageBinding> {
match self {
InputDefault::Image(b) => Some(b),
InputDefault::Value(_) => None,
}
}
}
impl From<Value> for InputDefault {
fn from(v: Value) -> Self {
InputDefault::Value(v)
}
}
impl From<ImageBinding> for InputDefault {
fn from(b: ImageBinding) -> Self {
InputDefault::Image(b)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InputSlot {
pub name: SharedStr,
pub default: InputDefault,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GraphNode {
pub id: NodeId,
pub payload: NodePayload,
pub inputs: Vec<InputSlot>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PortRef {
pub node: NodeId,
pub port: SharedStr,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GraphLink {
pub from: PortRef,
pub to: PortRef,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GraphStack {
pub id: StackId,
pub group: ModifierGroup,
pub members: Vec<NodeId>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PropertyDef {
pub id: PropertyId,
pub name: SharedStr,
pub default: Value,
#[serde(default)]
pub exposed: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EffectHeader {
pub name: SharedStr,
pub capacity: u32,
pub spawner: SpawnerSettings,
pub simulation_space: SimulationSpace,
pub simulation_condition: SimulationCondition,
pub z_layer_2d: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EffectGraph {
pub header: EffectHeader,
pub properties: Vec<PropertyDef>,
#[serde(default)]
pub texture_slots: Vec<TextureSlotDef>,
pub nodes: Vec<GraphNode>,
pub stacks: Vec<GraphStack>,
pub links: Vec<GraphLink>,
pub next_id: u32,
}
impl EffectGraph {
pub fn empty() -> Self {
Self {
header: EffectHeader {
name: "untitled".into(),
capacity: 4096,
spawner: SpawnerSettings::default(),
simulation_space: SimulationSpace::default(),
simulation_condition: SimulationCondition::default(),
z_layer_2d: 0.0,
},
properties: Vec::new(),
texture_slots: Vec::new(),
nodes: Vec::new(),
stacks: Vec::new(),
links: Vec::new(),
next_id: 1,
}
}
pub fn alloc_node_id(&mut self) -> NodeId {
let id = NodeId::new(self.next_id).expect("node id allocator overflow");
self.next_id += 1;
id
}
pub fn alloc_stack_id(&mut self) -> StackId {
let id = StackId::new(self.next_id).expect("stack id allocator overflow");
self.next_id += 1;
id
}
pub fn alloc_property_id(&mut self) -> PropertyId {
let id = PropertyId::new(self.next_id).expect("property id allocator overflow");
self.next_id += 1;
id
}
pub fn alloc_slot_id(&mut self) -> SlotId {
let id = SlotId::new(self.next_id).expect("slot id allocator overflow");
self.next_id += 1;
id
}
pub fn node(&self, id: NodeId) -> Option<&GraphNode> {
self.nodes.iter().find(|n| n.id == id)
}
pub fn node_mut(&mut self, id: NodeId) -> Option<&mut GraphNode> {
self.nodes.iter_mut().find(|n| n.id == id)
}
pub fn stack(&self, group: ModifierGroup) -> Option<&GraphStack> {
self.stacks.iter().find(|s| s.group == group)
}
pub fn property(&self, id: PropertyId) -> Option<&PropertyDef> {
self.properties.iter().find(|p| p.id == id)
}
pub fn texture_slot(&self, id: SlotId) -> Option<&TextureSlotDef> {
self.texture_slots.iter().find(|s| s.id == id)
}
pub fn texture_slot_index(&self, id: SlotId) -> Option<usize> {
self.texture_slots.iter().position(|s| s.id == id)
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct GraphLayout {
pub pan: (f64, f64),
pub zoom: f64,
pub node_pos: Vec<(NodeId, (f64, f64))>,
pub stack_pos: Vec<(StackId, (f64, f64))>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Asset, TypePath)]
pub struct EffectGraphAsset {
pub version: u32,
pub graph: EffectGraph,
pub layout: Option<GraphLayout>,
}
#[cfg(test)]
mod tests {
use super::*;
fn round_trip<T>(value: &T)
where
T: Serialize + for<'de> Deserialize<'de> + PartialEq + std::fmt::Debug,
{
let ron = ron::ser::to_string(value).expect("serialize");
let back: T = ron::de::from_str(&ron).unwrap_or_else(|e| panic!("deserialize {ron}: {e}"));
assert_eq!(value, &back, "round-trip mismatch via {ron}");
}
#[test]
fn edit_value_variants_round_trip() {
round_trip(&EditValue::Bool(true));
round_trip(&EditValue::U32(7));
round_trip(&EditValue::Scalar(Value::from(1.5f32)));
round_trip(&EditValue::UVec2(UVec2::new(2, 3)));
round_trip(&EditValue::Color(Vec4::new(1.0, 0.5, 0.25, 1.0)));
round_trip(&EditValue::Attribute(Attribute::LIFETIME));
round_trip(&EditValue::CpuVec3(CpuValue::Single(Vec3::ONE)));
round_trip(&EditValue::CpuVec4(CpuValue::Uniform((
Vec4::ZERO,
Vec4::ONE,
))));
round_trip(&EditValue::Gradient3(GradientVec3::Analytical(
Gradient::linear(Vec3::ZERO, Vec3::ONE),
)));
round_trip(&EditValue::Gradient4(GradientVec4::Lut(
TextureValue::Asset("ramps/fire.png".into()),
)));
round_trip(&EditValue::Texture(TextureValue::Slot {
name: "color".into(),
}));
round_trip(&EditValue::Enum {
type_path: "bevy_hanabi::modifier::ShapeDimension".into(),
variant: "Surface".into(),
});
round_trip(&EditValue::Flags {
type_path: "bevy_hanabi::modifier::output::ColorBlendMask".into(),
bits: 0b101,
});
round_trip(&EditValue::Raw("(some: \"future field\")".to_string()));
}
#[test]
fn modifier_node_data_round_trips() {
let mut config = BTreeMap::new();
config.insert(
"color".into(),
EditValue::CpuVec4(CpuValue::Single(Vec4::ONE)),
);
config.insert(
"blend".into(),
EditValue::Enum {
type_path: "bevy_hanabi::modifier::output::ColorBlendMode".into(),
variant: "Overwrite".into(),
},
);
round_trip(&ModifierNodeData::Known {
type_path: "bevy_hanabi::modifier::output::SetColorModifier".into(),
config,
});
round_trip(&ModifierNodeData::Unknown {
type_path: "my_crate::CustomModifier".into(),
raw: "(strength: 2.0)".to_string(),
});
}
#[test]
fn effect_graph_asset_round_trips() {
let n1 = NodeId::new(1).unwrap();
let n2 = NodeId::new(2).unwrap();
let stack = StackId::new(3).unwrap();
let speed = PropertyId::new(5).unwrap();
let tint = PropertyId::new(6).unwrap();
let slot = SlotId::new(7).unwrap();
let n_image = NodeId::new(8).unwrap();
let n_sample = NodeId::new(9).unwrap();
let graph = EffectGraph {
header: EffectHeader {
name: "demo".into(),
capacity: 4096,
spawner: SpawnerSettings::rate(64.0.into()),
simulation_space: SimulationSpace::Local,
simulation_condition: SimulationCondition::WhenVisible,
z_layer_2d: 0.0,
},
properties: vec![
PropertyDef {
id: speed,
name: "speed".into(),
default: Value::from(3.0f32),
exposed: true,
},
PropertyDef {
id: tint,
name: "tint".into(),
default: Value::from(Vec4::ONE),
exposed: false,
},
],
texture_slots: vec![TextureSlotDef {
id: slot,
name: "noise".into(),
}],
nodes: vec![
GraphNode {
id: n1,
payload: NodePayload::Expr(ExprNode::Property(speed)),
inputs: vec![],
},
GraphNode {
id: n2,
payload: NodePayload::Modifier(ModifierNodeData::Known {
type_path: "bevy_hanabi::modifier::velocity::SetVelocitySphereModifier"
.into(),
config: BTreeMap::new(),
}),
inputs: vec![InputSlot {
name: "speed".into(),
default: Value::from(1.0f32).into(),
}],
},
GraphNode {
id: n_image,
payload: NodePayload::Expr(ExprNode::Image(ImageBinding::Slot(slot))),
inputs: vec![],
},
GraphNode {
id: n_sample,
payload: NodePayload::Expr(ExprNode::TextureSample),
inputs: vec![InputSlot {
name: "coordinates".into(),
default: Value::from(bevy::math::Vec2::ZERO).into(),
}],
},
],
stacks: vec![GraphStack {
id: stack,
group: ModifierGroup::Init,
members: vec![n2],
}],
links: vec![
GraphLink {
from: PortRef {
node: n1,
port: "out".into(),
},
to: PortRef {
node: n2,
port: "speed".into(),
},
},
GraphLink {
from: PortRef {
node: n_image,
port: "out".into(),
},
to: PortRef {
node: n_sample,
port: "image".into(),
},
},
],
next_id: 10,
};
let asset = EffectGraphAsset {
version: FORMAT_VERSION,
graph,
layout: Some(GraphLayout {
pan: (10.0, -5.0),
zoom: 1.25,
node_pos: vec![(n1, (0.0, 0.0)), (n2, (200.0, 40.0))],
stack_pos: vec![(stack, (100.0, 300.0))],
}),
};
round_trip(&asset);
}
#[test]
fn expr_node_ports() {
assert_eq!(
ExprNode::Literal(Value::from(1.0f32)).input_ports(),
&[] as &[&str]
);
assert_eq!(ExprNode::Unary(UnaryOperator::Abs).input_ports(), &["in"]);
assert_eq!(
ExprNode::Binary(BinaryOperator::Add).input_ports(),
&["lhs", "rhs"]
);
assert_eq!(
ExprNode::Image(ImageBinding::Unbound).input_ports(),
&[] as &[&str]
);
assert_eq!(
ExprNode::TextureSample.input_ports(),
&["image", "coordinates"]
);
assert_eq!(
ExprNode::SelectImage { count: 1 }.input_ports(),
&["index", "image0"]
);
assert_eq!(
ExprNode::SelectImage { count: 3 }.input_ports(),
&["index", "image0", "image1", "image2"]
);
}
#[test]
fn select_image_inputs_are_image_typed() {
let n = ExprNode::SelectImage { count: 2 };
assert!(n.has_image_input());
assert!(!n.port_is_image("index"));
assert!(n.port_is_image("image0"));
assert!(n.port_is_image("image1"));
assert!(ExprNode::TextureSample.port_is_image("image"));
assert!(!ExprNode::TextureSample.port_is_image("coordinates"));
}
#[test]
fn operand_defaults_match_operator_type() {
use bevy::math::{Vec2, Vec3, Vec4};
let vt = |n: &ExprNode, p: &str| n.operand_default(p).as_value().map(|v| v.value_type());
let f32t = Some(ValueType::Scalar(ScalarType::Float));
assert_eq!(vt(&ExprNode::Binary(BinaryOperator::Add), "lhs"), f32t);
assert_eq!(
vt(&ExprNode::Binary(BinaryOperator::Cross), "rhs"),
Some(Value::from(Vec3::ZERO).value_type())
);
assert_eq!(
vt(&ExprNode::Binary(BinaryOperator::Dot), "lhs"),
Some(Value::from(Vec3::ZERO).value_type())
);
assert_eq!(
vt(&ExprNode::Binary(BinaryOperator::Vec4XyzW), "lhs"),
Some(Value::from(Vec3::ZERO).value_type())
);
assert_eq!(vt(&ExprNode::Binary(BinaryOperator::Vec4XyzW), "rhs"), f32t);
assert_eq!(
vt(&ExprNode::Unary(UnaryOperator::X), "in"),
Some(Value::from(Vec2::ZERO).value_type())
);
assert_eq!(
vt(&ExprNode::Unary(UnaryOperator::W), "in"),
Some(Value::from(Vec4::ZERO).value_type())
);
assert_eq!(
vt(&ExprNode::Unary(UnaryOperator::Pack4x8snorm), "in"),
Some(Value::from(Vec4::ZERO).value_type())
);
assert_eq!(
vt(&ExprNode::Unary(UnaryOperator::Unpack4x8snorm), "in"),
Some(ValueType::Scalar(ScalarType::Uint))
);
assert_eq!(
vt(&ExprNode::Unary(UnaryOperator::All), "in"),
Some(ValueType::Scalar(ScalarType::Bool))
);
assert!(
ExprNode::TextureSample
.operand_default("image")
.as_image()
.is_some()
);
}
#[test]
fn output_types_are_operator_aware() {
let vec3 = ValueType::Vector(VectorType::VEC3F);
let vec3_operand = |_: &str| Some(vec3);
assert_eq!(
ExprNode::Binary(BinaryOperator::Dot).output_value_type(vec3_operand),
Some(ValueType::Scalar(ScalarType::Float))
);
assert_eq!(
ExprNode::Binary(BinaryOperator::Cross).output_value_type(vec3_operand),
Some(vec3)
);
assert_eq!(
ExprNode::Unary(UnaryOperator::X).output_value_type(vec3_operand),
Some(ValueType::Scalar(ScalarType::Float))
);
assert_eq!(
ExprNode::Binary(BinaryOperator::GreaterThan).output_value_type(vec3_operand),
Some(ValueType::Vector(VectorType::new(ScalarType::Bool, 3)))
);
assert_eq!(
ExprNode::Ternary(TernaryOperator::Vec3)
.output_value_type(|_| Some(ValueType::Scalar(ScalarType::Float))),
Some(vec3)
);
assert_eq!(
ExprNode::Unary(UnaryOperator::Unpack4x8snorm)
.output_value_type(|_| Some(ValueType::Scalar(ScalarType::Uint))),
Some(ValueType::Vector(VectorType::VEC4F))
);
assert_eq!(
ExprNode::Binary(BinaryOperator::Add).output_value_type(vec3_operand),
Some(vec3)
);
}
#[test]
fn share_type_operators_are_element_wise_only() {
use BinaryOperator as B;
use TernaryOperator as T;
for op in [B::Add, B::Sub, B::Min, B::Max, B::Distance, B::GreaterThan] {
assert!(ExprNode::Binary(op).operands_share_type());
}
for op in [T::Mix, T::Clamp, T::SmoothStep] {
assert!(ExprNode::Ternary(op).operands_share_type());
}
assert!(!ExprNode::Binary(B::Mul).operands_share_type());
assert!(!ExprNode::Binary(B::Div).operands_share_type());
assert!(!ExprNode::Binary(B::Cross).operands_share_type());
assert!(!ExprNode::Binary(B::Vec2).operands_share_type());
assert!(!ExprNode::Ternary(T::Vec3).operands_share_type());
}
}