1225 lines
38 KiB
Rust
1225 lines
38 KiB
Rust
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<String>) -> Self {
|
||
Self(id.into())
|
||
}
|
||
}
|
||
|
||
/// 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<String>) -> 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<String>) -> 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 {
|
||
#[serde(default)]
|
||
pub component_type_names: Vec<String>,
|
||
#[serde(default)]
|
||
pub component_states: Vec<InspectorComponentState>,
|
||
}
|
||
|
||
/// 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 {
|
||
pub type_name: String,
|
||
#[serde(default = "default_true")]
|
||
pub active: bool,
|
||
}
|
||
|
||
impl Default for InspectorComponentState {
|
||
fn default() -> Self {
|
||
Self {
|
||
type_name: String::new(),
|
||
active: true,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl InspectorOrder {
|
||
pub fn is_component_active(&self, type_name: &str) -> bool {
|
||
self.component_states
|
||
.iter()
|
||
.find(|state| state.type_name == type_name)
|
||
.map(|state| state.active)
|
||
.unwrap_or(true)
|
||
}
|
||
|
||
pub fn set_component_active(&mut self, type_name: impl Into<String>, active: bool) {
|
||
let type_name = type_name.into();
|
||
if let Some(state) = self
|
||
.component_states
|
||
.iter_mut()
|
||
.find(|state| state.type_name == type_name)
|
||
{
|
||
state.active = active;
|
||
} else {
|
||
self.component_states
|
||
.push(InspectorComponentState { type_name, active });
|
||
}
|
||
}
|
||
|
||
pub fn ensure_component_order(&mut self, present: &[&str]) {
|
||
for type_name in present {
|
||
if !self
|
||
.component_type_names
|
||
.iter()
|
||
.any(|existing| existing == type_name)
|
||
{
|
||
self.component_type_names.push((*type_name).to_string());
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn ordered_components<'a>(&self, present: &[&'a str]) -> Vec<&'a str> {
|
||
let mut ordered = Vec::new();
|
||
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_type_names
|
||
.iter()
|
||
.position(|existing| existing == type_name)
|
||
else {
|
||
return false;
|
||
};
|
||
let target = (index as isize + offset)
|
||
.clamp(0, self.component_type_names.len() as isize - 1) as usize;
|
||
if target == index {
|
||
return false;
|
||
}
|
||
let item = self.component_type_names.remove(index);
|
||
self.component_type_names.insert(target, item);
|
||
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)
|
||
}
|
||
|
||
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";
|
||
|
||
/// 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,
|
||
}
|
||
|
||
impl EditorAssetRef {
|
||
pub fn new(
|
||
asset_id: impl Into<String>,
|
||
sub_asset_id: impl Into<String>,
|
||
label: impl Into<String>,
|
||
) -> Self {
|
||
Self {
|
||
asset_id: asset_id.into(),
|
||
sub_asset_id: sub_asset_id.into(),
|
||
label: label.into(),
|
||
}
|
||
}
|
||
|
||
pub fn is_resolved(&self) -> bool {
|
||
!self.asset_id.trim().is_empty() && !self.sub_asset_id.trim().is_empty()
|
||
}
|
||
}
|
||
|
||
/// 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<Vec3>,
|
||
#[serde(default)]
|
||
pub material: Option<EditorAssetRef>,
|
||
#[serde(default)]
|
||
pub texture: Option<EditorAssetRef>,
|
||
#[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<BrushFaceDesc>,
|
||
#[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,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Default for BrushDesc {
|
||
fn default() -> Self {
|
||
Self::cuboid(Vec3::ONE)
|
||
}
|
||
}
|
||
|
||
fn brush_face(id: &str, normal: Vec3, vertices: Vec<Vec3>) -> 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<String>,
|
||
/// Optional per-instance material override.
|
||
pub override_material: Option<MaterialDesc>,
|
||
}
|
||
|
||
/// 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,
|
||
#[serde(default)]
|
||
pub material: Option<EditorAssetRef>,
|
||
#[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: 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;
|
||
|
||
/// 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<MeshRenderSlot>,
|
||
}
|
||
|
||
impl StaticMeshRenderer {
|
||
pub fn empty() -> Self {
|
||
Self { slots: Vec::new() }
|
||
}
|
||
|
||
pub fn single(slot: MeshRenderSlot) -> Self {
|
||
Self { slots: vec![slot] }
|
||
}
|
||
}
|
||
|
||
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<String>,
|
||
#[serde(default)]
|
||
pub shader_path: Option<String>,
|
||
}
|
||
|
||
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<EditorAssetRef>,
|
||
}
|
||
|
||
/// 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<EditorAssetRef>,
|
||
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<MaterialSlotOverride>,
|
||
}
|
||
|
||
/// 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<String>,
|
||
/// Optional emissive texture, path relative to `assets/`.
|
||
#[serde(default)]
|
||
pub emissive_texture: Option<String>,
|
||
/// Optional normal map texture, path relative to `assets/`.
|
||
pub normal_map_texture: Option<String>,
|
||
/// Optional metallic/roughness texture, path relative to `assets/`.
|
||
pub metallic_roughness_texture: Option<String>,
|
||
/// When set, references a [`crate::MaterialAsset`] RON under `assets/materials/`.
|
||
#[serde(default)]
|
||
pub material_asset_path: Option<String>,
|
||
/// Shader-schema-driven scalar/vector/color values.
|
||
#[serde(default)]
|
||
pub parameters: Vec<MaterialParameter>,
|
||
/// Shader-schema-driven texture references.
|
||
#[serde(default)]
|
||
pub textures: Vec<MaterialTextureBinding>,
|
||
}
|
||
|
||
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 `SceneRoot` on the entity.
|
||
#[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)]
|
||
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
|
||
pub struct ModelRef {
|
||
/// 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<String>) -> Self {
|
||
Self {
|
||
path: path.into(),
|
||
scene_index: 0,
|
||
}
|
||
}
|
||
|
||
/// 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 `DynamicSceneRoot`.
|
||
#[derive(Component, Reflect, Default, Debug, Clone, 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<String>) -> Self {
|
||
Self { path: path.into() }
|
||
}
|
||
}
|
||
|
||
/// Marks a placed prefab instance with stable registry identity.
|
||
#[derive(Component, Reflect, Debug, Clone, Default, 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<String>,
|
||
}
|
||
|
||
impl PrefabInstance {
|
||
pub fn new(asset_id: impl Into<String>, source_path: impl Into<String>) -> 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<f32>,
|
||
pub fog_color: Option<ColorDesc>,
|
||
pub fog_density: Option<f32>,
|
||
pub bloom: Option<bool>,
|
||
pub taa: Option<bool>,
|
||
pub ssao: Option<bool>,
|
||
pub tonemapping_aces: Option<bool>,
|
||
pub atmosphere: Option<bool>,
|
||
pub hdr: Option<bool>,
|
||
}
|
||
|
||
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<String>,
|
||
/// Optional `assets/rendering_profiles/*.ron` preset seed.
|
||
pub profile: Option<String>,
|
||
/// Editor-friendly label for HUD / Rendering panel.
|
||
pub label: Option<String>,
|
||
}
|
||
|
||
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, 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,
|
||
ImportedModel,
|
||
Light,
|
||
PrefabAnchor,
|
||
PlayerSpawn,
|
||
WeaponSpawn,
|
||
TriggerVolume,
|
||
PostProcessVolume,
|
||
TeamSpawn,
|
||
Objective,
|
||
}
|
||
|
||
/// 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<EditorAssetRef>,
|
||
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<EditorAssetRef>) -> Self {
|
||
Self::StaticMesh {
|
||
meshes,
|
||
cooking: MeshColliderCooking::Default,
|
||
}
|
||
}
|
||
|
||
pub fn to_constructor(&self) -> Option<ColliderConstructor> {
|
||
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<EditorAssetRef>) -> 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::ModelRef;
|
||
use bevy::light::light_consts;
|
||
|
||
#[test]
|
||
fn point_spot_lumen_max_covers_bevy_cinema_reference() {
|
||
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"
|
||
);
|
||
}
|
||
}
|