use uuid::Uuid;
use super::{
assets::AssetId, camera::CameraConfig, collider::ColliderShape, curve::CurveDef,
decal::DecalConfig, dynamic_material::MaterialInstance, instances::InstancesAlongCurveDef,
light::LightConfig, line::LineDef, particle::ParticleEmitterDef, primitive::MeshRef,
sprite::SpriteDef, transform::Trs,
};
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct EditorNode {
pub id: NodeId,
pub name: String,
#[serde(default)]
pub transform: Trs,
pub kind: NodeKind,
#[serde(default)]
pub locked: bool,
#[serde(default = "default_visible")]
pub visible: bool,
#[serde(default)]
pub prefab: bool,
#[serde(default)]
pub children: Vec<EditorNode>,
}
fn default_visible() -> bool {
true
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
#[derive(Eq, Hash, Copy)]
pub struct NodeId(pub Uuid);
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for NodeId {
fn schema_name() -> std::borrow::Cow<'static, str> {
"NodeId".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({ "type": "string", "format": "uuid" })
}
}
impl NodeId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
pub const fn nil() -> Self {
Self(Uuid::nil())
}
pub fn is_nil(&self) -> bool {
self.0.is_nil()
}
pub fn as_bytes(&self) -> &[u8; 16] {
self.0.as_bytes()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, NodeIdFromBytesError> {
Uuid::from_slice(bytes)
.map(Self)
.map_err(|_| NodeIdFromBytesError {
got_len: bytes.len(),
})
}
}
#[derive(Debug, thiserror::Error)]
#[error("NodeId::from_bytes: expected 16 bytes, got {got_len}")]
pub struct NodeIdFromBytesError {
pub got_len: usize,
}
impl Default for NodeId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for NodeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SkinnedMeshRef {
pub source: AssetId,
pub node_index: u32,
#[serde(default)]
pub rig_node_index: u32,
#[serde(default)]
pub primitive_index: Option<u32>,
#[serde(default)]
pub joints: Vec<SkinJoint>,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SkinJoint {
pub node: NodeId,
pub index: u32,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ClusterMeshRef {
pub source: AssetId,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[allow(clippy::large_enum_variant)]
pub enum NodeKind {
Group,
Light(LightConfig),
Collider(ColliderShape),
Camera(CameraConfig),
Mesh {
mesh: MeshRef,
#[serde(default)]
material: Option<MaterialInstance>,
#[serde(default)]
shadow: MeshShadowConfig,
#[serde(default)]
lod: MeshLodConfig,
},
SkinnedMesh {
skin: SkinnedMeshRef,
#[serde(default)]
material: Option<MaterialInstance>,
#[serde(default)]
shadow: MeshShadowConfig,
#[serde(default)]
lod: MeshLodConfig,
},
ClusterMesh {
cluster: ClusterMeshRef,
#[serde(default)]
material: Option<MaterialInstance>,
#[serde(default)]
shadow: MeshShadowConfig,
},
Curve(CurveDef),
InstancesAlongCurve(InstancesAlongCurveDef),
Line(LineDef),
Sprite(SpriteDef),
ParticleEmitter(ParticleEmitterDef),
Decal(DecalConfig),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct MeshShadowConfig {
#[serde(default = "default_true_msc")]
pub cast: bool,
#[serde(default = "default_true_msc")]
pub receive: bool,
}
impl Default for MeshShadowConfig {
fn default() -> Self {
Self {
cast: true,
receive: true,
}
}
}
impl MeshShadowConfig {
pub const TRANSPARENT_DEFAULT: Self = Self {
cast: false,
receive: false,
};
}
fn default_true_msc() -> bool {
true
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct MeshLodConfig {
#[serde(default = "default_true_mlc")]
pub enabled: bool,
}
impl Default for MeshLodConfig {
fn default() -> Self {
Self { enabled: true }
}
}
fn default_true_mlc() -> bool {
true
}
impl NodeKind {
pub fn label(&self) -> &'static str {
match self {
Self::Group => "group",
Self::Light(_) => "light",
Self::Collider(_) => "collider",
Self::Camera(_) => "camera",
Self::Mesh { .. } => "mesh",
Self::SkinnedMesh { .. } => "skinned_mesh",
Self::ClusterMesh { .. } => "cluster_mesh",
Self::Curve(_) => "curve",
Self::InstancesAlongCurve(_) => "instances",
Self::Line(_) => "line",
Self::Sprite(_) => "sprite",
Self::ParticleEmitter(_) => "particle",
Self::Decal(_) => "decal",
}
}
pub fn mesh_shadow(&self) -> Option<&MeshShadowConfig> {
match self {
Self::Mesh { shadow, .. } => Some(shadow),
Self::SkinnedMesh { shadow, .. } => Some(shadow),
Self::InstancesAlongCurve(d) => Some(&d.shadow),
_ => None,
}
}
pub fn mesh_shadow_mut(&mut self) -> Option<&mut MeshShadowConfig> {
match self {
Self::Mesh { shadow, .. } => Some(shadow),
Self::SkinnedMesh { shadow, .. } => Some(shadow),
Self::InstancesAlongCurve(d) => Some(&mut d.shadow),
_ => None,
}
}
pub fn mesh_lod(&self) -> Option<&MeshLodConfig> {
match self {
Self::Mesh { lod, .. } => Some(lod),
Self::SkinnedMesh { lod, .. } => Some(lod),
Self::InstancesAlongCurve(d) => Some(&d.lod),
_ => None,
}
}
pub fn mesh_lod_mut(&mut self) -> Option<&mut MeshLodConfig> {
match self {
Self::Mesh { lod, .. } => Some(lod),
Self::SkinnedMesh { lod, .. } => Some(lod),
Self::InstancesAlongCurve(d) => Some(&mut d.lod),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skinned_mesh_joints_toml_round_trip() {
let node = EditorNode {
id: NodeId::new(),
name: "Cylinder".into(),
transform: Trs::default(),
kind: NodeKind::SkinnedMesh {
skin: SkinnedMeshRef {
source: AssetId::new(),
node_index: 2,
rig_node_index: 2,
primitive_index: None,
joints: vec![
SkinJoint {
node: NodeId::new(),
index: 3,
},
SkinJoint {
node: NodeId::new(),
index: 4,
},
],
},
material: None,
shadow: MeshShadowConfig::default(),
lod: MeshLodConfig::default(),
},
locked: false,
visible: true,
prefab: false,
children: Vec::new(),
};
let text = toml::to_string(&node).expect("serialize");
let back: EditorNode = toml::from_str(&text).expect("deserialize");
assert_eq!(node, back);
match back.kind {
NodeKind::SkinnedMesh { skin, .. } => {
assert_eq!(skin.joints.len(), 2);
assert_eq!(skin.joints[0].index, 3);
assert_eq!(skin.joints[1].index, 4);
}
other => panic!("expected SkinnedMesh, got {other:?}"),
}
}
#[test]
fn skinned_mesh_ref_joints_default_empty() {
let toml = r#"
source = "00000000-0000-0000-0000-000000000000"
node_index = 1
"#;
let skin: SkinnedMeshRef = toml::from_str(toml).expect("deserialize legacy");
assert!(skin.joints.is_empty());
assert_eq!(skin.node_index, 1);
}
#[test]
fn mesh_lod_config_default_and_round_trip() {
let off = MeshLodConfig { enabled: false };
let text = toml::to_string(&off).expect("serialize");
let back: MeshLodConfig = toml::from_str(&text).expect("deserialize");
assert_eq!(off, back);
let legacy = r#"
[mesh]
mesh = "00000000-0000-0000-0000-000000000000"
"#;
let kind: NodeKind = toml::from_str(legacy).expect("deserialize legacy mesh kind");
assert_eq!(
kind.mesh_lod().copied(),
Some(MeshLodConfig { enabled: true }),
"absent `lod` must default to enabled (opt-out, default on)"
);
}
}
#[cfg(all(test, feature = "schemars"))]
mod schema_tests {
use crate::NodeKind;
#[test]
fn node_kind_schema_lists_variant_fields() {
let json = serde_json::to_string(&schemars::schema_for!(NodeKind)).unwrap();
for needle in [
"ParticleEmitterDef",
"spawn_rate", "CameraConfig",
"projection", "LightConfig", "MaterialInstance", "AnisotropyExt", ] {
assert!(json.contains(needle), "NodeKind schema missing `{needle}`");
}
}
}