use avian3d::prelude::ColliderConstructor; use bevy::prelude::*; use serde::{Deserialize, Serialize}; /// Stable persisted identity for an actor. Bevy [`Entity`] IDs are runtime-only. #[derive( Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, )] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct ActorId(pub String); impl ActorId { pub fn new(id: impl Into) -> Self { Self(id.into()) } } /// Stable scene-document identity and its authored subscene references. /// /// This is persisted as a reflected scene resource. Cross-scene ownership never /// depends on runtime [`Entity`] identifiers. #[derive(Resource, Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Resource, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct SceneComposition { pub scene_id: String, #[serde(default)] pub subscenes: Vec, } /// One stable, project-relative reference in a [`SceneComposition`]. #[derive(Reflect, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct SubsceneReference { pub id: String, pub path: String, #[serde(default = "default_true")] pub visible: bool, #[serde(default = "default_true")] pub locked: bool, } impl Default for SubsceneReference { fn default() -> Self { Self { id: String::new(), path: String::new(), visible: true, locked: true, } } } /// User-facing actor display name, separate from Bevy's runtime [`Name`]. #[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct ActorName(pub String); impl ActorName { pub fn new(name: impl Into) -> Self { Self(name.into()) } } /// Stable identity for repeatable authoring items such as renderer slots. #[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct ComponentInstanceId(pub String); impl ComponentInstanceId { pub fn new(id: impl Into) -> Self { Self(id.into()) } pub fn is_empty(&self) -> bool { self.0.trim().is_empty() } } /// Editor-only component ordering metadata. Runtime systems must not depend on it. #[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct InspectorOrder { /// Stable component IDs used by current scenes. #[serde(default)] pub component_ids: Vec, /// Legacy reflected type paths used by schema-v3 scenes. #[serde(default)] pub component_type_names: Vec, /// Legacy schema-v3 storage. New authoring writes active state to /// [`AuthoringComponentStates`]; this field remains readable so old scenes can /// be upgraded without losing disabled components. #[serde(default)] pub component_states: Vec, } /// Persisted active state for authoring components. /// /// This is intentionally separate from [`InspectorOrder`]: hydration and runtime /// behavior must not depend on how the editor happens to arrange component cards. #[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct AuthoringComponentStates { #[serde(default)] pub states: Vec, } /// Per-component inspector metadata that must survive scene save/load. #[derive(Reflect, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct InspectorComponentState { /// Stable authoring component identity used by current scenes. #[serde(default)] pub component_id: String, /// Legacy schema-v3 reflected type path. #[serde(default)] pub type_name: String, #[serde(default = "default_true")] pub active: bool, } impl Default for InspectorComponentState { fn default() -> Self { Self { component_id: String::new(), type_name: String::new(), active: true, } } } impl InspectorOrder { pub fn is_component_active(&self, type_name: &str) -> bool { let component_id = authoring_component_key(type_name); self.component_states .iter() .find(|state| { state.component_id == component_id || state.type_name == type_name || state.type_name == component_id }) .map(|state| state.active) .unwrap_or(true) } pub fn set_component_active(&mut self, type_name: impl Into, active: bool) { let type_name = type_name.into(); let component_id = authoring_component_key(&type_name).to_string(); if let Some(state) = self.component_states.iter_mut().find(|state| { state.component_id == component_id || state.type_name == type_name || state.type_name == component_id }) { state.component_id = component_id; state.type_name.clear(); state.active = active; } else { self.component_states.push(InspectorComponentState { component_id, type_name: String::new(), active, }); } } pub fn ensure_component_order(&mut self, present: &[&str]) { for type_name in present { let component_id = authoring_component_key(type_name); if !self .component_ids .iter() .any(|existing| existing == component_id) { self.component_ids.push(component_id.to_string()); } } // Once the editor touches order, the stable-ID representation is // authoritative and legacy paths no longer need to be re-saved. self.component_type_names.clear(); } pub fn ordered_components<'a>(&self, present: &[&'a str]) -> Vec<&'a str> { let mut ordered = Vec::new(); for component_id in &self.component_ids { if let Some(present_type) = present .iter() .find(|present| authoring_component_key(present) == component_id) { ordered.push(*present_type); } } for type_name in &self.component_type_names { if let Some(present_type) = present.iter().find(|present| **present == type_name) { ordered.push(*present_type); } } for type_name in present { if !ordered.iter().any(|ordered_type| ordered_type == type_name) { ordered.push(*type_name); } } ordered } pub fn move_component(&mut self, type_name: &str, offset: isize, present: &[&str]) -> bool { self.ensure_component_order(present); let Some(index) = self .component_ids .iter() .position(|existing| existing == authoring_component_key(type_name)) else { return false; }; let target = (index as isize + offset).clamp(0, self.component_ids.len() as isize - 1) as usize; if target == index { return false; } let item = self.component_ids.remove(index); self.component_ids.insert(target, item); true } } impl AuthoringComponentStates { pub fn is_component_active(&self, type_name: &str) -> bool { let component_id = authoring_component_key(type_name); self.states .iter() .find(|state| { state.component_id == component_id || state.type_name == type_name || state.type_name == component_id }) .map(|state| state.active) .unwrap_or(true) } pub fn set_component_active(&mut self, type_name: impl Into, active: bool) { let type_name = type_name.into(); let component_id = authoring_component_key(&type_name).to_string(); if let Some(state) = self.states.iter_mut().find(|state| { state.component_id == component_id || state.type_name == type_name || state.type_name == component_id }) { state.component_id = component_id; state.type_name.clear(); state.active = active; } else { self.states.push(InspectorComponentState { component_id, type_name: String::new(), active, }); } } pub fn remove_component(&mut self, type_name: &str) { let component_id = authoring_component_key(type_name); self.states.retain(|state| { state.component_id != component_id && state.type_name != type_name && state.type_name != component_id }); } } /// Returns whether a persisted authoring component is enabled. /// /// `legacy_order` is consulted only when an older scene has not yet been migrated /// to [`AuthoringComponentStates`]. pub fn authoring_component_active( states: Option<&AuthoringComponentStates>, legacy_order: Option<&InspectorOrder>, type_name: &str, ) -> bool { states .map(|states| states.is_component_active(type_name)) .or_else(|| legacy_order.map(|order| order.is_component_active(type_name))) .unwrap_or(true) } pub fn inspector_component_active(order: Option<&InspectorOrder>, type_name: &str) -> bool { order .map(|order| order.is_component_active(type_name)) .unwrap_or(true) } // Stable authoring component identities. These are editor/protocol keys; reflected // Rust type paths remain serialization details and may change during refactors. pub const AUTHORING_COMPONENT_PRIMITIVE: &str = "render.primitive"; pub const AUTHORING_COMPONENT_BRUSH: &str = "render.brush"; pub const AUTHORING_COMPONENT_STATIC_MESH_RENDERER: &str = "render.static_mesh_renderer"; pub const AUTHORING_COMPONENT_SKINNED_MESH_RENDERER: &str = "render.skinned_mesh_renderer"; pub const AUTHORING_COMPONENT_MATERIAL: &str = "render.material"; pub const AUTHORING_COMPONENT_LIGHT: &str = "render.light"; pub const AUTHORING_COMPONENT_ANIMATION_CONTROLLER: &str = "animation.controller"; pub const AUTHORING_COMPONENT_AUDIO_SOURCE: &str = "audio.source"; pub const AUTHORING_COMPONENT_AUDIO_LISTENER: &str = "audio.listener"; pub const AUTHORING_COMPONENT_RIGID_BODY: &str = "physics.rigid_body"; pub const AUTHORING_COMPONENT_COLLIDER: &str = "physics.collider"; pub const AUTHORING_COMPONENT_LEGACY_PHYSICS_BODY: &str = "physics.legacy_body"; pub const AUTHORING_COMPONENT_PLAYER_SPAWN: &str = "gameplay.player_spawn"; pub const AUTHORING_COMPONENT_WEAPON_SPAWN: &str = "gameplay.weapon_spawn"; pub const AUTHORING_COMPONENT_TRIGGER_VOLUME: &str = "gameplay.trigger_volume"; pub const AUTHORING_COMPONENT_TEAM_SPAWN: &str = "gameplay.team_spawn"; pub const AUTHORING_COMPONENT_OBJECTIVE: &str = "gameplay.objective"; pub const AUTHORING_COMPONENT_POST_PROCESS_VOLUME: &str = "render.post_process_volume"; pub const AUTHORING_COMPONENT_NAVIGATION_BOUNDS: &str = "navigation.bounds"; pub const AUTHORING_COMPONENT_NAVIGATION_OBSTACLE: &str = "navigation.obstacle"; pub const AUTHORING_COMPONENT_NAVIGATION_AREA: &str = "navigation.area"; pub const AUTHORING_COMPONENT_NAVIGATION_LINK: &str = "navigation.link"; /// Resolves either a stable authoring ID or a reflected Rust type path to its /// stable editor/protocol identity. Unknown extension keys are returned as /// `None` and remain usable as-is by generic state/order storage. pub fn authoring_component_id(key: &str) -> Option<&'static str> { match key { AUTHORING_COMPONENT_PRIMITIVE | COMPONENT_PRIMITIVE => Some(AUTHORING_COMPONENT_PRIMITIVE), AUTHORING_COMPONENT_BRUSH | COMPONENT_BRUSH_DESC => Some(AUTHORING_COMPONENT_BRUSH), AUTHORING_COMPONENT_STATIC_MESH_RENDERER | COMPONENT_STATIC_MESH_RENDERER => { Some(AUTHORING_COMPONENT_STATIC_MESH_RENDERER) } AUTHORING_COMPONENT_SKINNED_MESH_RENDERER | "shared::animation::SkinnedMeshRenderer" => { Some(AUTHORING_COMPONENT_SKINNED_MESH_RENDERER) } AUTHORING_COMPONENT_MATERIAL | COMPONENT_MATERIAL_DESC => { Some(AUTHORING_COMPONENT_MATERIAL) } AUTHORING_COMPONENT_LIGHT | COMPONENT_LIGHT_DESC => Some(AUTHORING_COMPONENT_LIGHT), AUTHORING_COMPONENT_ANIMATION_CONTROLLER | "shared::animation::AnimationControllerDesc" => { Some(AUTHORING_COMPONENT_ANIMATION_CONTROLLER) } AUTHORING_COMPONENT_AUDIO_SOURCE | COMPONENT_AUDIO_SOURCE_DESC => { Some(AUTHORING_COMPONENT_AUDIO_SOURCE) } AUTHORING_COMPONENT_AUDIO_LISTENER | COMPONENT_AUDIO_LISTENER_DESC => { Some(AUTHORING_COMPONENT_AUDIO_LISTENER) } AUTHORING_COMPONENT_RIGID_BODY | COMPONENT_RIGID_BODY_DESC => { Some(AUTHORING_COMPONENT_RIGID_BODY) } AUTHORING_COMPONENT_COLLIDER | COMPONENT_COLLIDER_DESC => { Some(AUTHORING_COMPONENT_COLLIDER) } AUTHORING_COMPONENT_LEGACY_PHYSICS_BODY | COMPONENT_PHYSICS_BODY => { Some(AUTHORING_COMPONENT_LEGACY_PHYSICS_BODY) } AUTHORING_COMPONENT_PLAYER_SPAWN | COMPONENT_PLAYER_SPAWN => { Some(AUTHORING_COMPONENT_PLAYER_SPAWN) } AUTHORING_COMPONENT_WEAPON_SPAWN | COMPONENT_WEAPON_SPAWN => { Some(AUTHORING_COMPONENT_WEAPON_SPAWN) } AUTHORING_COMPONENT_TRIGGER_VOLUME | COMPONENT_TRIGGER_VOLUME => { Some(AUTHORING_COMPONENT_TRIGGER_VOLUME) } AUTHORING_COMPONENT_TEAM_SPAWN | COMPONENT_TEAM_SPAWN => { Some(AUTHORING_COMPONENT_TEAM_SPAWN) } AUTHORING_COMPONENT_OBJECTIVE | COMPONENT_OBJECTIVE_MARKER => { Some(AUTHORING_COMPONENT_OBJECTIVE) } AUTHORING_COMPONENT_POST_PROCESS_VOLUME | COMPONENT_POST_PROCESS_VOLUME => { Some(AUTHORING_COMPONENT_POST_PROCESS_VOLUME) } AUTHORING_COMPONENT_NAVIGATION_BOUNDS | "shared::navigation::NavigationBounds" => { Some(AUTHORING_COMPONENT_NAVIGATION_BOUNDS) } AUTHORING_COMPONENT_NAVIGATION_OBSTACLE | "shared::navigation::NavigationObstacle" => { Some(AUTHORING_COMPONENT_NAVIGATION_OBSTACLE) } AUTHORING_COMPONENT_NAVIGATION_AREA | "shared::navigation::NavigationArea" => { Some(AUTHORING_COMPONENT_NAVIGATION_AREA) } AUTHORING_COMPONENT_NAVIGATION_LINK | "shared::navigation::NavigationLink" => { Some(AUTHORING_COMPONENT_NAVIGATION_LINK) } _ => None, } } pub fn authoring_component_key(key: &str) -> &str { authoring_component_id(key).unwrap_or(key) } pub const COMPONENT_PRIMITIVE: &str = "shared::components::Primitive"; pub const COMPONENT_BRUSH_DESC: &str = "shared::components::BrushDesc"; pub const COMPONENT_STATIC_MESH_RENDERER: &str = "shared::components::StaticMeshRenderer"; pub const COMPONENT_MATERIAL_DESC: &str = "shared::components::MaterialDesc"; pub const COMPONENT_MATERIAL_OVERRIDE: &str = "shared::components::MaterialOverride"; pub const COMPONENT_LIGHT_DESC: &str = "shared::components::LightDesc"; pub const COMPONENT_RIGID_BODY_DESC: &str = "shared::components::RigidBodyDesc"; pub const COMPONENT_COLLIDER_DESC: &str = "shared::components::ColliderDesc"; pub const COMPONENT_PHYSICS_BODY: &str = "shared::components::PhysicsBody"; pub const COMPONENT_PLAYER_SPAWN: &str = "shared::components::PlayerSpawn"; pub const COMPONENT_WEAPON_SPAWN: &str = "shared::components::WeaponSpawn"; pub const COMPONENT_TRIGGER_VOLUME: &str = "shared::components::TriggerVolume"; pub const COMPONENT_TEAM_SPAWN: &str = "shared::components::TeamSpawn"; pub const COMPONENT_OBJECTIVE_MARKER: &str = "shared::components::ObjectiveMarker"; pub const COMPONENT_PREFAB_INSTANCE: &str = "shared::components::PrefabInstance"; pub const COMPONENT_POST_PROCESS_VOLUME: &str = "shared::components::PostProcessVolumeDesc"; pub const COMPONENT_PROJECT_SUN: &str = "shared::components::ProjectSun"; pub const COMPONENT_AUDIO_SOURCE_DESC: &str = "shared::components::AudioSourceDesc"; pub const COMPONENT_AUDIO_LISTENER_DESC: &str = "shared::components::AudioListenerDesc"; /// Stable imported sub-asset identifier used by authored audio clip references. pub const AUDIO_CLIP_SUB_ASSET_ID: &str = "audio:source"; /// Reference to an imported content-browser asset or sub-asset. #[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct EditorAssetRef { /// Stable project asset registry UUID string. pub asset_id: String, /// Stable imported sub-asset ID inside the generated artifact. #[serde(default, alias = "mesh_label")] pub sub_asset_id: String, /// UI label cached for inspectors. The asset registry/artifact remains authoritative. #[serde(default)] pub label: String, /// Optional loadable project path cached for runtime hydration when the /// referenced asset is not available through an imported artifact lookup. #[serde(default)] pub source_path: Option, } impl EditorAssetRef { pub fn new( asset_id: impl Into, sub_asset_id: impl Into, label: impl Into, ) -> Self { Self { asset_id: asset_id.into(), sub_asset_id: sub_asset_id.into(), label: label.into(), source_path: None, } } pub fn with_source_path(mut self, source_path: impl Into) -> Self { self.source_path = Some(source_path.into()); self } pub fn is_resolved(&self) -> bool { !self.asset_id.trim().is_empty() && !self.sub_asset_id.trim().is_empty() } } /// Distance curve used by an authored spatial audio source. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum AudioRolloff { Linear, #[default] Inverse, Exponential, } /// Serializable distance attenuation parameters for spatial audio playback. #[derive(Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct AudioAttenuationDesc { /// Radius in meters inside which the source plays at full gain. #[serde(default = "default_audio_min_distance")] pub min_distance: f32, /// Radius in meters at which distance attenuation reaches silence. #[serde(default = "default_audio_max_distance")] pub max_distance: f32, #[serde(default)] pub rolloff: AudioRolloff, /// Curve strength. `1.0` is the neutral authored value. #[serde(default = "default_audio_rolloff_factor")] pub rolloff_factor: f32, } impl Default for AudioAttenuationDesc { fn default() -> Self { Self { min_distance: default_audio_min_distance(), max_distance: default_audio_max_distance(), rolloff: AudioRolloff::default(), rolloff_factor: default_audio_rolloff_factor(), } } } fn default_audio_min_distance() -> f32 { 1.0 } fn default_audio_max_distance() -> f32 { 25.0 } fn default_audio_rolloff_factor() -> f32 { 1.0 } fn default_audio_pitch() -> f32 { 1.0 } fn default_audio_spatial_blend() -> f32 { 1.0 } fn default_audio_bus() -> String { settings::AUDIO_BUS_SFX_ID.to_string() } fn default_audio_ear_gap() -> f32 { 0.2 } /// Scene-authored audio source. Runtime playback components are derived from this descriptor. #[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct AudioSourceDesc { #[serde(default)] pub clip: Option, /// Source gain in decibels before bus gain is applied. #[serde(default)] pub gain_db: f32, #[serde(default = "default_audio_pitch")] pub pitch: f32, #[serde(default)] pub looping: bool, #[serde(default = "default_true")] pub autoplay: bool, /// Continuous authored blend where `0.0` is 2D and `1.0` is fully spatial. #[serde(default = "default_audio_spatial_blend")] pub spatial_blend: f32, #[serde(default)] pub attenuation: AudioAttenuationDesc, /// Stable project audio bus ID. #[serde(default = "default_audio_bus")] pub bus: String, } impl Default for AudioSourceDesc { fn default() -> Self { Self { clip: None, gain_db: 0.0, pitch: default_audio_pitch(), looping: false, autoplay: true, spatial_blend: default_audio_spatial_blend(), attenuation: AudioAttenuationDesc::default(), bus: default_audio_bus(), } } } /// Scene-authored spatial listener. Runtime selection resolves enabled listeners by priority. #[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct AudioListenerDesc { #[serde(default = "default_true")] pub enabled: bool, #[serde(default)] pub priority: i32, /// Distance in meters between the listener's virtual ears. #[serde(default = "default_audio_ear_gap")] pub ear_gap: f32, } impl Default for AudioListenerDesc { fn default() -> Self { Self { enabled: true, priority: 0, ear_gap: default_audio_ear_gap(), } } } /// Marker for entities that belong to editable/savable level data. #[derive(Component, Reflect, Default, Debug, Clone, Copy, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct LevelObject; /// Runtime marker for helper meshes that must not enter Solari raytracing. #[derive(Component, Reflect, Default, Debug, Clone, Copy)] #[reflect(Component, Default, Debug)] pub struct RaytracingExcluded; /// Marks the player's spawn location for editor Play mode. /// /// Place on a level object and set its `Transform` to choose where Play starts. /// If none exists, the game default spawn is used. #[derive(Component, Reflect, Default, Debug, Clone, Copy, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct PlayerSpawn; /// Marks the runtime/project default directional sun. /// /// Scene-authored directional [`LightDesc`] entities can override this default /// so levels can own their lighting without duplicating sun contribution. #[derive(Component, Reflect, Default, Debug, Clone, Copy)] #[reflect(Component, Default, Debug)] pub struct ProjectSun; /// Simple primitive meshes that can be authored without external assets. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum PrimitiveShape { #[default] Box, Sphere, Ramp, } /// Reflectable authoring primitive. Hydration turns this into `Mesh3d`. #[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct Primitive { pub shape: PrimitiveShape, pub size: Vec3, } impl Primitive { pub fn cuboid(size: Vec3) -> Self { Self { shape: PrimitiveShape::Box, size, } } pub fn sphere(radius: f32) -> Self { Self { shape: PrimitiveShape::Sphere, size: Vec3::splat(radius * 2.0), } } pub fn ramp(size: Vec3) -> Self { Self { shape: PrimitiveShape::Ramp, size, } } } impl Default for Primitive { fn default() -> Self { Self::cuboid(Vec3::ONE) } } /// Brush boolean role. Subtractive brushes are authored now but CSG is applied /// by later tooling. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum BrushKind { #[default] Additive, SubtractiveMarker, } /// Serializable convex brush plane. Plane equation is `normal.dot(point) = distance`. #[derive(Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct BrushPlaneDesc { pub normal: Vec3, pub distance: f32, } impl Default for BrushPlaneDesc { fn default() -> Self { Self { normal: Vec3::Y, distance: 0.0, } } } /// One polygon face on a convex authoring brush. #[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct BrushFaceDesc { #[serde(default)] pub id: ComponentInstanceId, pub plane: BrushPlaneDesc, #[serde(default)] pub vertices: Vec, #[serde(default)] pub material: Option, #[serde(default)] pub texture: Option, #[serde(default)] pub uv_offset: Vec2, #[serde(default = "default_uv_scale")] pub uv_scale: Vec2, #[serde(default)] pub uv_rotation: f32, #[serde(default)] pub smoothing_group: u32, } impl Default for BrushFaceDesc { fn default() -> Self { Self { id: ComponentInstanceId::default(), plane: BrushPlaneDesc::default(), vertices: Vec::new(), material: None, texture: None, uv_offset: Vec2::ZERO, uv_scale: default_uv_scale(), uv_rotation: 0.0, smoothing_group: 0, } } } fn default_uv_scale() -> Vec2 { Vec2::ONE } /// Convex brush authoring data. Hydration turns this into generated mesh /// children while the scene stores only this component. #[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct BrushDesc { pub kind: BrushKind, #[serde(default)] pub faces: Vec, #[serde(default = "default_true")] pub cast_shadows: bool, #[serde(default = "default_true")] pub receive_shadows: bool, } impl BrushDesc { pub fn cuboid(size: Vec3) -> Self { let hx = size.x.max(0.001) * 0.5; let hy = size.y.max(0.001) * 0.5; let hz = size.z.max(0.001) * 0.5; let faces = vec![ brush_face( "face:+x", Vec3::X, vec![ Vec3::new(hx, -hy, -hz), Vec3::new(hx, hy, -hz), Vec3::new(hx, hy, hz), Vec3::new(hx, -hy, hz), ], ), brush_face( "face:-x", Vec3::NEG_X, vec![ Vec3::new(-hx, -hy, hz), Vec3::new(-hx, hy, hz), Vec3::new(-hx, hy, -hz), Vec3::new(-hx, -hy, -hz), ], ), brush_face( "face:+y", Vec3::Y, vec![ Vec3::new(-hx, hy, -hz), Vec3::new(-hx, hy, hz), Vec3::new(hx, hy, hz), Vec3::new(hx, hy, -hz), ], ), brush_face( "face:-y", Vec3::NEG_Y, vec![ Vec3::new(-hx, -hy, hz), Vec3::new(-hx, -hy, -hz), Vec3::new(hx, -hy, -hz), Vec3::new(hx, -hy, hz), ], ), brush_face( "face:+z", Vec3::Z, vec![ Vec3::new(hx, -hy, hz), Vec3::new(hx, hy, hz), Vec3::new(-hx, hy, hz), Vec3::new(-hx, -hy, hz), ], ), brush_face( "face:-z", Vec3::NEG_Z, vec![ Vec3::new(-hx, -hy, -hz), Vec3::new(-hx, hy, -hz), Vec3::new(hx, hy, -hz), Vec3::new(hx, -hy, -hz), ], ), ]; Self { kind: BrushKind::Additive, faces, cast_shadows: true, receive_shadows: true, } } pub fn extruded_prism(base_vertices: &[Vec3], height: f32) -> Option { let mut base = crate::brush_math::normalize_floor_polygon(base_vertices).ok()?; crate::brush_math::validate_floor_polygon(&base).ok()?; let height = height.max(0.001); if crate::brush_math::signed_area_xz(&base) < 0.0 { base.reverse(); } let top: Vec = base .iter() .map(|vertex| *vertex + Vec3::Y * height) .collect(); let mut top_face = top; top_face.reverse(); let mut faces = Vec::with_capacity(base.len() + 2); faces.push(brush_face("face:+y", Vec3::Y, top_face)); faces.push(brush_face("face:-y", Vec3::NEG_Y, base.clone())); for index in 0..base.len() { let next = (index + 1) % base.len(); let a = base[index]; let b = base[next]; let edge = b - a; let normal = Vec3::new(edge.z, 0.0, -edge.x).normalize_or_zero(); if normal.length_squared() <= f32::EPSILON { return None; } faces.push(brush_face( &format!("face:side:{index}"), normal, vec![a, a + Vec3::Y * height, b + Vec3::Y * height, b], )); } Some(Self { kind: BrushKind::Additive, faces, cast_shadows: true, receive_shadows: true, }) } } impl Default for BrushDesc { fn default() -> Self { Self::cuboid(Vec3::ONE) } } fn brush_face(id: &str, normal: Vec3, vertices: Vec) -> BrushFaceDesc { let distance = vertices .first() .map(|vertex| normal.dot(*vertex)) .unwrap_or(0.0); BrushFaceDesc { id: ComponentInstanceId::new(id), plane: BrushPlaneDesc { normal, distance }, vertices, ..Default::default() } } /// Legacy static mesh collision setting kept only for loading older scenes. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum StaticMeshColliderMode { #[default] None, /// Generate an Avian triangle mesh collider from the loaded render mesh. TrimeshFromMesh, } /// Backward-compatible alias for the previous static mesh reference name. pub type StaticMeshAssetRef = EditorAssetRef; /// One material slot on a static mesh renderer entry. #[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct StaticMeshMaterialSlot { pub slot_index: usize, pub name: String, /// Bevy sub-asset label for the source material, if the importer provided one. pub source_material_label: Option, /// Optional per-instance material override. pub override_material: Option, } /// One mesh draw slot owned by a [`StaticMeshRenderer`]. #[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct MeshRenderSlot { #[serde(default)] pub id: ComponentInstanceId, pub name: String, pub mesh: EditorAssetRef, /// Stable renderer material slot used by this draw part. #[serde(default)] pub material_slot_id: ComponentInstanceId, /// Legacy inline imported-material reference. Schema-v4 migration moves this into the /// renderer's shared `RendererMaterialSet`. #[serde(default)] pub material: Option, #[serde(default)] pub local_transform: Transform, #[serde(default = "default_true")] pub visible: bool, #[serde(default = "default_true")] pub cast_shadows: bool, #[serde(default = "default_true")] pub receive_shadows: bool, } impl Default for MeshRenderSlot { fn default() -> Self { Self { id: ComponentInstanceId::default(), name: "Mesh".into(), mesh: EditorAssetRef::default(), material_slot_id: ComponentInstanceId::default(), material: None, local_transform: Transform::default(), visible: true, cast_shadows: true, receive_shadows: true, } } } /// Backward-compatible alias for old code paths while scenes migrate to slots. pub type StaticMeshRendererEntry = MeshRenderSlot; /// Public renderer-foundation name for one static draw part. pub type StaticMeshPart = MeshRenderSlot; /// Reflectable static mesh renderer. Hydration turns entries into child `Mesh3d` /// entities and source/default materials. #[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct StaticMeshRenderer { #[serde(default, alias = "entries")] pub slots: Vec, #[serde(default)] pub materials: crate::RendererMaterialSet, } impl StaticMeshRenderer { pub fn empty() -> Self { Self { slots: Vec::new(), materials: crate::RendererMaterialSet::default(), } } pub fn single(mut slot: MeshRenderSlot) -> Self { if slot.material_slot_id.0.trim().is_empty() { slot.material_slot_id = ComponentInstanceId::new(format!("slot:{}", slot.id.0)); } let source_material = slot.material.clone().map(crate::MaterialRef::new); let materials = crate::RendererMaterialSet { slots: vec![crate::RendererMaterialSlot { id: slot.material_slot_id.clone(), name: slot.name.clone(), source_material, material: None, }], orphaned_assignments: Vec::new(), }; Self { slots: vec![slot], materials, } } pub fn material_slot_for_part( &self, part: &MeshRenderSlot, ) -> Option<&crate::RendererMaterialSlot> { let slot_id = if part.material_slot_id.0.trim().is_empty() { &part.id } else { &part.material_slot_id }; self.materials.slot(slot_id) } } impl Default for StaticMeshRenderer { fn default() -> Self { Self::empty() } } fn default_true() -> bool { true } /// How a material should be resolved at runtime. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum MaterialShaderKind { #[default] StandardLit, Unlit, Custom, } /// Reference to a built-in or custom shader schema. #[derive(Reflect, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct ShaderRefDesc { pub kind: MaterialShaderKind, #[serde(default)] pub schema_path: Option, #[serde(default)] pub shader_path: Option, } impl Default for ShaderRefDesc { fn default() -> Self { Self { kind: MaterialShaderKind::StandardLit, schema_path: None, shader_path: None, } } } /// Typed material parameter value exposed by a shader schema. #[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum MaterialParameterValue { Bool(bool), Float(f32), Vec2(Vec2), Vec3(Vec3), Color(ColorDesc), Enum(String), } impl Default for MaterialParameterValue { fn default() -> Self { Self::Float(0.0) } } #[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct MaterialParameter { pub name: String, pub value: MaterialParameterValue, } #[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct MaterialTextureBinding { pub name: String, pub texture: Option, } /// Per-render-slot material override stored on scene actors. #[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub struct MaterialSlotOverride { pub slot_id: ComponentInstanceId, #[serde(default)] pub base_material: Option, pub material: MaterialDesc, } /// Actor-level material overrides. Shared material assets are edited by asset /// inspectors, while actor inspectors write only to this component. #[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct MaterialOverride { #[serde(default)] pub slots: Vec, } /// Serializable color description. This avoids coupling saved scenes to any /// internal color representation details. #[derive(Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, Serialize, Deserialize)] pub struct ColorDesc { pub r: f32, pub g: f32, pub b: f32, pub a: f32, } impl ColorDesc { pub const fn srgb(r: f32, g: f32, b: f32) -> Self { Self { r, g, b, a: 1.0 } } pub fn to_color(self) -> Color { Color::srgba(self.r, self.g, self.b, self.a) } } impl Default for ColorDesc { fn default() -> Self { Self::srgb(0.8, 0.8, 0.8) } } /// Reflectable authoring material. Hydration turns this into `StandardMaterial`. #[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct MaterialDesc { #[serde(default)] pub shader: ShaderRefDesc, pub base_color: ColorDesc, pub metallic: f32, pub roughness: f32, /// Emissive radiance tint. Values are multiplied by `emissive_intensity`. #[serde(default = "default_emissive_color")] pub emissive_color: ColorDesc, /// Emissive luminance multiplier in nits. `0.0` means non-emissive. #[serde(default)] pub emissive_intensity: f32, /// Optional base color (albedo) texture, path relative to `assets/`. pub base_color_texture: Option, /// Optional emissive texture, path relative to `assets/`. #[serde(default)] pub emissive_texture: Option, /// Optional normal map texture, path relative to `assets/`. pub normal_map_texture: Option, /// Optional metallic/roughness texture, path relative to `assets/`. pub metallic_roughness_texture: Option, /// When set, references a [`crate::MaterialAsset`] RON under `assets/materials/`. #[serde(default)] pub material_asset_path: Option, /// Shader-schema-driven scalar/vector/color values. #[serde(default)] pub parameters: Vec, /// Shader-schema-driven texture references. #[serde(default)] pub textures: Vec, } impl MaterialDesc { pub fn new(base_color: ColorDesc, metallic: f32, roughness: f32) -> Self { Self { shader: ShaderRefDesc::default(), base_color, metallic, roughness, emissive_color: default_emissive_color(), emissive_intensity: 0.0, base_color_texture: None, emissive_texture: None, normal_map_texture: None, metallic_roughness_texture: None, material_asset_path: None, parameters: Vec::new(), textures: Vec::new(), } } } fn default_emissive_color() -> ColorDesc { ColorDesc::srgb(1.0, 1.0, 1.0) } impl Default for MaterialDesc { fn default() -> Self { Self::new(ColorDesc::default(), 0.0, 0.65) } } /// Reflectable reference to an imported 3D model scene (glTF/GLB or FBX). /// Hydration turns this into a `WorldAssetRoot` on the entity. #[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct ModelRef { /// Stable asset-registry UUID. Empty only for legacy or unresolved references. #[serde(default)] pub asset_id: String, /// Asset path relative to `assets/`, e.g. `models/foo.glb` or `models/foo.fbx`. pub path: String, /// Scene index within the model file (`#SceneN` for FBX; glTF scene index otherwise). pub scene_index: usize, } impl ModelRef { pub fn new(path: impl Into) -> Self { Self { asset_id: String::new(), path: path.into(), scene_index: 0, } } pub fn with_asset_id(mut self, asset_id: impl Into) -> Self { self.asset_id = asset_id.into(); self } /// Asset-server path for FBX `#SceneN` labels (see `bevy_ufbx`). pub fn fbx_scene_asset_path(path: &str, scene_index: usize) -> String { format!("{path}#Scene{scene_index}") } } /// Reflectable reference to a saved `.scn.ron` dynamic scene under `assets/`. /// Hydration turns this into a `DynamicWorldRoot`. #[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct PrefabRef { /// Asset path relative to `assets/`, e.g. "prefabs/foo.scn.ron". pub path: String, } impl PrefabRef { pub fn new(path: impl Into) -> Self { Self { path: path.into() } } } /// Marks a placed prefab instance with stable registry identity. #[derive(Component, Reflect, Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct PrefabInstance { /// Stable asset registry UUID string. pub asset_id: String, /// Source prefab path relative to `assets/`. pub source_path: String, /// Optional per-instance transform override serialized as RON (v1: unused). #[serde(default)] pub overrides_ron: Option, } impl PrefabInstance { pub fn new(asset_id: impl Into, source_path: impl Into) -> Self { Self { asset_id: asset_id.into(), source_path: source_path.into(), overrides_ron: None, } } } /// Weapon pickup / spawn pad for FPS gameplay authoring. #[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct WeaponSpawn { pub weapon_id: String, } /// Per-field post-process overrides (`None` = inherit project settings). #[derive(Reflect, Debug, Clone, Default, Serialize, Deserialize)] #[reflect(Default, Debug, Serialize, Deserialize)] pub struct PostProcessVolumeOverrides { pub exposure_ev100: Option, pub fog_color: Option, pub fog_density: Option, pub bloom: Option, pub taa: Option, pub ssao: Option, pub tonemapping_aces: Option, pub atmosphere: Option, pub hdr: Option, } impl PostProcessVolumeOverrides { pub fn override_count(&self) -> u32 { let mut count = 0u32; if self.exposure_ev100.is_some() { count += 1; } if self.fog_color.is_some() { count += 1; } if self.fog_density.is_some() { count += 1; } if self.bloom.is_some() { count += 1; } if self.taa.is_some() { count += 1; } if self.ssao.is_some() { count += 1; } if self.tonemapping_aces.is_some() { count += 1; } if self.atmosphere.is_some() { count += 1; } if self.hdr.is_some() { count += 1; } count } } /// Scene-authored post-process volume (axis-aligned box in local space). #[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct PostProcessVolumeDesc { pub half_extents: Vec3, pub priority: i32, pub blend_distance: f32, pub overrides: PostProcessVolumeOverrides, /// Path to `assets/post_fx/*.ron`, if any. pub fullscreen_effect: Option, /// Optional `assets/rendering_profiles/*.ron` preset seed. pub profile: Option, /// Editor-friendly label for HUD / Rendering panel. pub label: Option, } impl Default for PostProcessVolumeDesc { fn default() -> Self { Self { half_extents: Vec3::new(4.0, 2.0, 4.0), priority: 0, blend_distance: 2.0, overrides: PostProcessVolumeOverrides::default(), fullscreen_effect: None, profile: None, label: None, } } } /// Axis-aligned trigger volume for gameplay events. #[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct TriggerVolume { pub half_extents: Vec3, pub event_name: String, } impl Default for TriggerVolume { fn default() -> Self { Self { half_extents: Vec3::new(2.0, 2.0, 2.0), event_name: "trigger".into(), } } } /// Team spawn point marker. #[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct TeamSpawn { pub team_id: u8, } /// Objective / game mode marker. #[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct ObjectiveMarker { pub objective_id: String, } /// Stable sibling order among entities sharing the same parent in the hierarchy outliner. #[derive(Component, Reflect, Default, Debug, Clone, Copy, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct HierarchySiblingIndex(pub i32); /// Authoring visibility saved with the scene; hydration maps this to ECS [`Visibility`]. #[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct EditorVisibility { pub visible: bool, } impl Default for EditorVisibility { fn default() -> Self { Self { visible: true } } } /// Declared role of a level object for inspector routing and save validation. #[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Component, Debug, PartialEq, Serialize, Deserialize)] pub enum ActorKind { Empty, Brush, StaticMesh, SkinnedMesh, ImportedModel, Light, PrefabAnchor, PlayerSpawn, WeaponSpawn, TriggerVolume, PostProcessVolume, TeamSpawn, Objective, AudioSource, AudioListener, Navigation, } /// Inspector upper bound for point/spot [`LightDesc::intensity`] (lumens). /// /// Bevy's `PointLight` / `SpotLight` default is `light_consts::lumens::VERY_LARGE_CINEMA_LIGHT` /// (1M lumens) for outdoor exposure (~12–15 EV100). This cap is 2× that reference so authored /// lights can exceed the old 200k ceiling without unbounded sliders. pub const AUTHORING_POINT_SPOT_LUMENS_MAX: f32 = 2_000_000.0; /// Inspector upper bound for directional [`LightDesc::intensity`] (lux). pub const AUTHORING_DIRECTIONAL_LUX_MAX: f32 = 150_000.0; /// Serializable light kinds for editor-authored lighting. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum AuthoringLightKind { #[default] Point, Spot, Directional, } /// Reflectable authoring light. Hydration turns this into Bevy light components. #[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct LightDesc { pub kind: AuthoringLightKind, pub color: ColorDesc, /// Lumens (point/spot) or lux (directional). pub intensity: f32, /// Attenuation range in meters (point/spot). pub range: f32, pub shadows: bool, /// Spot inner cone half-angle in degrees. #[serde(default = "default_spot_inner_angle_deg")] pub inner_angle_deg: f32, /// Spot outer cone half-angle in degrees. #[serde(default = "default_spot_outer_angle_deg")] pub outer_angle_deg: f32, } fn default_spot_inner_angle_deg() -> f32 { 25.0 } fn default_spot_outer_angle_deg() -> f32 { 35.0 } impl Default for LightDesc { fn default() -> Self { Self::for_kind(AuthoringLightKind::Point) } } impl LightDesc { /// Sensible defaults when spawning a builtin light from the asset browser. pub fn for_kind(kind: AuthoringLightKind) -> Self { match kind { AuthoringLightKind::Point => Self { kind, color: ColorDesc::srgb(1.0, 0.95, 0.85), intensity: 25_000.0, range: 25.0, shadows: true, inner_angle_deg: default_spot_inner_angle_deg(), outer_angle_deg: default_spot_outer_angle_deg(), }, AuthoringLightKind::Spot => Self { kind, color: ColorDesc::srgb(1.0, 0.95, 0.85), intensity: 30_000.0, range: 35.0, shadows: true, inner_angle_deg: default_spot_inner_angle_deg(), outer_angle_deg: default_spot_outer_angle_deg(), }, AuthoringLightKind::Directional => Self { kind, color: ColorDesc::srgb(1.0, 0.95, 0.85), // Matches `ProjectSettings::rendering.sun_illuminance` default; spawn code // refreshes this from live project settings when placing in the scene. intensity: 100_000.0, range: 0.0, shadows: true, inner_angle_deg: default_spot_inner_angle_deg(), outer_angle_deg: default_spot_outer_angle_deg(), }, } } } /// Runtime body type in a serialized-friendly form. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum AuthoringRigidBody { #[default] Static, Kinematic, Dynamic, } /// Reflectable authoring rigid body separated from collider geometry. #[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct RigidBodyDesc { pub body: AuthoringRigidBody, } impl Default for RigidBodyDesc { fn default() -> Self { Self { body: AuthoringRigidBody::Static, } } } /// Mesh collider cooking options for imported static mesh assets. #[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum MeshColliderCooking { #[default] Default, } /// Serializable collider shape independent of rigid body settings. #[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Default, Debug, PartialEq, Serialize, Deserialize)] pub enum ColliderShapeDesc { Cuboid { x_length: f32, y_length: f32, z_length: f32, }, Sphere { radius: f32, }, Capsule { radius: f32, height: f32, }, StaticMesh { meshes: Vec, cooking: MeshColliderCooking, }, } impl Default for ColliderShapeDesc { fn default() -> Self { Self::Cuboid { x_length: 1.0, y_length: 1.0, z_length: 1.0, } } } impl ColliderShapeDesc { pub fn static_mesh(meshes: Vec) -> Self { Self::StaticMesh { meshes, cooking: MeshColliderCooking::Default, } } pub fn to_constructor(&self) -> Option { match self { ColliderShapeDesc::Cuboid { x_length, y_length, z_length, } => Some(ColliderConstructor::Cuboid { x_length: *x_length, y_length: *y_length, z_length: *z_length, }), ColliderShapeDesc::Sphere { radius } => { Some(ColliderConstructor::Sphere { radius: *radius }) } ColliderShapeDesc::Capsule { radius, height } => Some(ColliderConstructor::Capsule { radius: *radius, height: *height, }), ColliderShapeDesc::StaticMesh { .. } => None, } } } /// Reflectable authoring collider. Static mesh colliders are hydrated on /// generated mesh children so they can reuse Bevy's mesh-to-collider path. #[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)] #[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct ColliderDesc { #[serde(default = "default_true")] pub enabled: bool, #[serde(default)] pub is_trigger: bool, pub shape: ColliderShapeDesc, } impl ColliderDesc { pub fn static_cuboid(size: Vec3) -> Self { Self { enabled: true, is_trigger: false, shape: ColliderShapeDesc::Cuboid { x_length: size.x, y_length: size.y, z_length: size.z, }, } } pub fn static_sphere(radius: f32) -> Self { Self { enabled: true, is_trigger: false, shape: ColliderShapeDesc::Sphere { radius }, } } pub fn static_mesh(meshes: Vec) -> Self { Self { enabled: true, is_trigger: false, shape: ColliderShapeDesc::static_mesh(meshes), } } } impl Default for ColliderDesc { fn default() -> Self { Self::static_cuboid(Vec3::ONE) } } /// Reflectable authoring physics. Hydration turns this into `RigidBody` and /// Avian's reflectable `ColliderConstructor` component. #[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)] #[reflect(Component, Default, Debug, Serialize, Deserialize)] pub struct PhysicsBody { pub body: AuthoringRigidBody, pub collider: ColliderConstructor, } impl PhysicsBody { pub fn static_cuboid(size: Vec3) -> Self { Self { body: AuthoringRigidBody::Static, collider: ColliderConstructor::Cuboid { x_length: size.x, y_length: size.y, z_length: size.z, }, } } pub fn static_sphere(radius: f32) -> Self { Self { body: AuthoringRigidBody::Static, collider: ColliderConstructor::Sphere { radius }, } } } impl Default for PhysicsBody { fn default() -> Self { Self::static_cuboid(Vec3::ONE) } } #[cfg(test)] mod tests { use super::{ AudioListenerDesc, AudioRolloff, AudioSourceDesc, EditorAssetRef, ModelRef, AUDIO_CLIP_SUB_ASSET_ID, }; use bevy::light::light_consts; #[test] fn point_spot_lumen_max_covers_bevy_cinema_reference() { const { assert!( super::AUTHORING_POINT_SPOT_LUMENS_MAX >= light_consts::lumens::VERY_LARGE_CINEMA_LIGHT ); } } #[test] fn fbx_scene_asset_path_format() { assert_eq!( ModelRef::fbx_scene_asset_path("models/foo.fbx", 0), "models/foo.fbx#Scene0" ); assert_eq!( ModelRef::fbx_scene_asset_path("models/foo.fbx", 2), "models/foo.fbx#Scene2" ); } #[test] fn legacy_model_ref_without_asset_id_remains_readable() { let model: ModelRef = ron::from_str( r#"( path: "assets/models/legacy.glb", scene_index: 2, )"#, ) .expect("legacy ModelRef should deserialize"); assert!(model.asset_id.is_empty()); assert_eq!(model.path, "assets/models/legacy.glb"); assert_eq!(model.scene_index, 2); } #[test] fn audio_authoring_descriptors_round_trip() { let source = AudioSourceDesc { clip: Some( EditorAssetRef::new("clip-id", AUDIO_CLIP_SUB_ASSET_ID, "Alarm") .with_source_path("assets/audio/alarm.ogg"), ), gain_db: -3.0, pitch: 0.9, looping: true, ..Default::default() }; let serialized = ron::to_string(&(source.clone(), AudioListenerDesc::default())).unwrap(); let (parsed_source, parsed_listener): (AudioSourceDesc, AudioListenerDesc) = ron::from_str(&serialized).unwrap(); assert_eq!(parsed_source, source); assert_eq!(parsed_listener, AudioListenerDesc::default()); assert_eq!(parsed_source.attenuation.rolloff, AudioRolloff::Inverse); assert_eq!(parsed_source.bus, settings::AUDIO_BUS_SFX_ID); } }