use bevy::{
math::{Vec2, Vec3, Vec4},
reflect::{TypeInfo, Typed},
};
use bevy_hanabi::{ScalarType, Value, ValueType};
use super::model::SharedStr;
pub const OUTPUT_PORT: &str = "out";
pub fn value_type_zero(ty: ValueType) -> Option<Value> {
Some(match ty {
ValueType::Scalar(ScalarType::Float) => Value::from(0.0f32),
ValueType::Scalar(ScalarType::Int) => Value::from(0i32),
ValueType::Scalar(ScalarType::Uint) => Value::from(0u32),
ValueType::Scalar(ScalarType::Bool) => Value::from(false),
ValueType::Vector(v) => match (v.elem_type(), v.count()) {
(ScalarType::Float, 2) => Value::from(Vec2::ZERO),
(ScalarType::Float, 3) => Value::from(Vec3::ZERO),
(ScalarType::Float, 4) => Value::from(Vec4::ZERO),
_ => return None,
},
_ => return None,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigKind {
Bool,
U32,
Scalar,
UVec2,
Attribute,
CpuVec3,
CpuVec4,
Gradient3,
Gradient4,
Enum,
Flags,
Raw,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldRole {
ExprPort { optional: bool },
Texture,
Config(ConfigKind),
}
#[derive(Debug, Clone)]
pub struct FieldSchema {
pub name: SharedStr,
pub type_path: &'static str,
pub role: FieldRole,
}
#[derive(Debug, Clone)]
pub struct ModifierSchema {
pub type_path: &'static str,
pub fields: Vec<FieldSchema>,
}
impl ModifierSchema {
pub fn ports(&self) -> impl Iterator<Item = &FieldSchema> {
self.fields
.iter()
.filter(|f| matches!(f.role, FieldRole::ExprPort { .. } | FieldRole::Texture))
}
pub fn config(&self) -> impl Iterator<Item = &FieldSchema> {
self.fields
.iter()
.filter(|f| matches!(f.role, FieldRole::Config(_)))
}
}
pub fn modifier_schema_of<T: Typed>() -> Option<ModifierSchema> {
modifier_schema(T::type_info())
}
pub fn modifier_schema(info: &TypeInfo) -> Option<ModifierSchema> {
let TypeInfo::Struct(s) = info else {
return None;
};
let type_path = s.type_path();
let fields = s
.iter()
.map(|f| FieldSchema {
name: SharedStr::from(f.name()),
type_path: f.type_path(),
role: classify_field(type_path, f.name(), f.type_path(), f.type_info()),
})
.collect();
Some(ModifierSchema { type_path, fields })
}
fn classify_field(
modifier_path: &str,
field_name: &str,
field_path: &str,
field_info: Option<&TypeInfo>,
) -> FieldRole {
if is_expr_handle(field_path) && is_texture_field(modifier_path, field_name) {
return FieldRole::Texture;
}
if let Some(optional) = expr_port_kind(field_path) {
return FieldRole::ExprPort { optional };
}
FieldRole::Config(config_kind(field_path, field_info))
}
fn base_name(path: &str) -> &str {
let head = path.split('<').next().unwrap_or(path);
head.rsplit("::").next().unwrap_or(head)
}
fn is_expr_handle(path: &str) -> bool {
base_name(path) == "ExprHandle"
}
fn expr_port_kind(path: &str) -> Option<bool> {
if is_expr_handle(path) {
Some(false)
} else if base_name(path) == "Option" && path.contains("ExprHandle") {
Some(true)
} else {
None
}
}
fn is_texture_field(modifier_path: &str, field_name: &str) -> bool {
base_name(modifier_path) == "ParticleTextureModifier" && field_name == "texture_slot"
}
#[derive(Debug, Clone)]
pub struct FlagDef {
pub name: &'static str,
pub bits: u64,
}
pub fn flag_defs(type_path: &str) -> Vec<FlagDef> {
if base_name(type_path) == "ColorBlendMask" {
use bevy_hanabi::ColorBlendMask;
return vec![
FlagDef {
name: "R",
bits: ColorBlendMask::R.bits() as u64,
},
FlagDef {
name: "G",
bits: ColorBlendMask::G.bits() as u64,
},
FlagDef {
name: "B",
bits: ColorBlendMask::B.bits() as u64,
},
FlagDef {
name: "A",
bits: ColorBlendMask::A.bits() as u64,
},
];
}
Vec::new()
}
fn config_kind(path: &str, info: Option<&TypeInfo>) -> ConfigKind {
match base_name(path) {
"bool" => ConfigKind::Bool,
"u32" => ConfigKind::U32,
"UVec2" => ConfigKind::UVec2,
"f32" | "i32" | "i64" | "u64" | "Vec2" | "Vec3" | "Vec4" | "IVec2" | "IVec3" | "IVec4"
| "UVec3" | "UVec4" => ConfigKind::Scalar,
"Attribute" => ConfigKind::Attribute,
"CpuValue" if path.contains("Vec3") => ConfigKind::CpuVec3,
"CpuValue" if path.contains("Vec4") => ConfigKind::CpuVec4,
"Gradient" if path.contains("Vec3") => ConfigKind::Gradient3,
"Gradient" if path.contains("Vec4") => ConfigKind::Gradient4,
"ColorBlendMask" => ConfigKind::Flags,
_ if matches!(info, Some(TypeInfo::Enum(_))) => ConfigKind::Enum,
_ => ConfigKind::Raw,
}
}
#[cfg(test)]
mod tests {
use bevy_hanabi::{
ColorOverLifetimeModifier, ConformToSphereModifier, ParticleTextureModifier,
SetColorModifier, SetPositionSphereModifier, SizeOverLifetimeModifier, Value,
};
use super::*;
fn role_of<'a>(schema: &'a ModifierSchema, name: &str) -> &'a FieldRole {
&schema
.fields
.iter()
.find(|f| &*f.name == name)
.unwrap_or_else(|| panic!("field {name} not found in {}", schema.type_path))
.role
}
#[test]
fn set_color_modifier_fields() {
let s = modifier_schema_of::<SetColorModifier>().unwrap();
assert_eq!(
*role_of(&s, "color"),
FieldRole::Config(ConfigKind::CpuVec4)
);
assert_eq!(*role_of(&s, "blend"), FieldRole::Config(ConfigKind::Enum));
assert_eq!(*role_of(&s, "mask"), FieldRole::Config(ConfigKind::Flags));
assert_eq!(s.ports().count(), 0);
}
#[test]
fn position_sphere_ports_and_enum() {
let s = modifier_schema_of::<SetPositionSphereModifier>().unwrap();
assert_eq!(
*role_of(&s, "center"),
FieldRole::ExprPort { optional: false }
);
assert_eq!(
*role_of(&s, "radius"),
FieldRole::ExprPort { optional: false }
);
assert_eq!(
*role_of(&s, "dimension"),
FieldRole::Config(ConfigKind::Enum)
);
assert_eq!(s.ports().count(), 2);
}
#[test]
fn texture_slot_is_a_texture_port() {
let s = modifier_schema_of::<ParticleTextureModifier>().unwrap();
assert_eq!(*role_of(&s, "texture_slot"), FieldRole::Texture);
assert_eq!(
*role_of(&s, "sample_mapping"),
FieldRole::Config(ConfigKind::Enum)
);
assert_eq!(s.ports().count(), 1);
assert_eq!(s.ports().next().unwrap().name.as_ref(), "texture_slot");
}
#[test]
fn optional_expr_handle_is_optional_port() {
let s = modifier_schema_of::<ConformToSphereModifier>().unwrap();
assert_eq!(
*role_of(&s, "shell_half_thickness"),
FieldRole::ExprPort { optional: true }
);
assert_eq!(
*role_of(&s, "origin"),
FieldRole::ExprPort { optional: false }
);
}
#[test]
fn gradients_and_bool() {
let s = modifier_schema_of::<SizeOverLifetimeModifier>().unwrap();
assert_eq!(
*role_of(&s, "gradient"),
FieldRole::Config(ConfigKind::Gradient3)
);
assert_eq!(
*role_of(&s, "screen_space_size"),
FieldRole::Config(ConfigKind::Bool)
);
let c = modifier_schema_of::<ColorOverLifetimeModifier>().unwrap();
assert_eq!(
*role_of(&c, "gradient"),
FieldRole::Config(ConfigKind::Gradient4)
);
}
#[test]
fn value_type_zero_covers_seedable_types() {
use bevy::math::{Vec2, Vec3, Vec4};
let cases = [
Value::from(0.0f32),
Value::from(0i32),
Value::from(0u32),
Value::from(false),
Value::from(Vec2::ZERO),
Value::from(Vec3::ZERO),
Value::from(Vec4::ZERO),
];
for expected in cases {
assert_eq!(value_type_zero(expected.value_type()), Some(expected));
}
}
}