nightshade-api 0.53.0

Procedural high level API for the nightshade game engine
Documentation
//! Reading back what the setters write: per-entity material getters, the
//! quaternion/euler conversions an inspector needs, and [`describe_entity`],
//! which gathers an entity's whole editable state into one [`EntityView`].

use crate::reflect::{ComponentKind, addable_components, present_components};
use nightshade::ecs::animation::components::AnimationPlayer;
use nightshade::ecs::camera::components::Camera;
use nightshade::ecs::decal::components::Decal;
use nightshade::ecs::primitives::{CameraCullingMask, CullingMask};
use nightshade::ecs::text::components::TextProperties;
use nightshade::plugins::audio::components::AudioSource;
use nightshade::prelude::*;
use nightshade::render::material::Material;
use nightshade::render::particles::ParticleEmitter;
use nightshade::render::render_layer::RenderLayer;
use serde::{Deserialize, Serialize};

/// Converts a rotation quaternion to intrinsic XYZ euler angles in radians, the
/// roll/pitch/yaw an inspector edits.
pub fn quat_to_euler_xyz(quat: &Quat) -> (f32, f32, f32) {
    let sinr = 2.0 * (quat.w * quat.i + quat.j * quat.k);
    let cosr = 1.0 - 2.0 * (quat.i * quat.i + quat.j * quat.j);
    let roll = sinr.atan2(cosr);

    let sinp = 2.0 * (quat.w * quat.j - quat.k * quat.i);
    let pitch = if sinp.abs() >= 1.0 {
        std::f32::consts::FRAC_PI_2.copysign(sinp)
    } else {
        sinp.asin()
    };

    let siny = 2.0 * (quat.w * quat.k + quat.i * quat.j);
    let cosy = 1.0 - 2.0 * (quat.j * quat.j + quat.k * quat.k);
    let yaw = siny.atan2(cosy);

    (roll, pitch, yaw)
}

/// Builds a rotation quaternion from intrinsic XYZ euler angles in radians, the
/// inverse of [`quat_to_euler_xyz`].
pub fn euler_xyz_to_quat(roll: f32, pitch: f32, yaw: f32) -> Quat {
    let cr = (roll * 0.5).cos();
    let sr = (roll * 0.5).sin();
    let cp = (pitch * 0.5).cos();
    let sp = (pitch * 0.5).sin();
    let cy = (yaw * 0.5).cos();
    let sy = (yaw * 0.5).sin();

    Quat::from_parts(
        cr * cp * cy + sr * sp * sy,
        nalgebra_glm::Vec3::new(
            sr * cp * cy - cr * sp * sy,
            cr * sp * cy + sr * cp * sy,
            cr * cp * sy - sr * sp * cy,
        ),
    )
}

/// The entity's material, if it has one. A clone of the registry entry its
/// `MaterialRef` names, so reads do not borrow the registry.
pub fn material_of(world: &World, entity: Entity) -> Option<Material> {
    let material_ref = world.get::<nightshade::ecs::material::components::MaterialRef>(entity)?;
    registry_entry_by_name(
        &world.resources.assets.material_registry.registry,
        &material_ref.name,
    )
    .cloned()
}

/// The entity's base color, the read side of
/// [`set_color`](crate::prelude::set_color).
pub fn color(world: &World, entity: Entity) -> Option<[f32; 4]> {
    material_of(world, entity).map(|material| material.base_color)
}

/// The entity's metallic and roughness factors.
pub fn metallic_roughness(world: &World, entity: Entity) -> Option<(f32, f32)> {
    material_of(world, entity).map(|material| (material.metallic, material.roughness))
}

/// The entity's emissive color and strength.
pub fn emissive(world: &World, entity: Entity) -> Option<([f32; 3], f32)> {
    material_of(world, entity)
        .map(|material| (material.emissive_factor, material.emissive_strength))
}

/// Whether the entity's material renders unlit.
pub fn unlit(world: &World, entity: Entity) -> Option<bool> {
    material_of(world, entity).map(|material| material.unlit)
}

/// The name of the entity's base color texture, if it has one.
pub fn texture(world: &World, entity: Entity) -> Option<String> {
    material_of(world, entity).and_then(|material| material.base_texture)
}

/// The entity's animation player state, summarized for an inspector.
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct AnimationView {
    pub clips: Vec<String>,
    pub current: Option<u32>,
    pub playing: bool,
    pub time: f32,
    pub duration: f32,
    pub speed: f32,
    pub looping: bool,
}

/// Reads an entity's animation player into an [`AnimationView`].
pub fn animation_view(player: &AnimationPlayer) -> AnimationView {
    let duration = player
        .get_current_clip()
        .map(|clip| clip.duration)
        .unwrap_or(0.0);
    AnimationView {
        clips: player.clips.iter().map(|clip| clip.name.clone()).collect(),
        current: player.current_clip.map(|index| index as u32),
        playing: player.playing,
        time: player.time,
        duration,
        speed: player.speed,
        looping: player.looping,
    }
}

/// An entity's whole editable state in one value: transform as euler degrees,
/// looks, every optional component it carries, and the lists of which
/// components it has and could gain. The mirror of the spawn-and-set API,
/// gathered for an inspector or a binding to render.
#[derive(Clone, Serialize, Deserialize)]
pub struct EntityView {
    pub id: u32,
    pub name: String,
    pub translation: [f32; 3],
    pub rotation: [f32; 3],
    pub scale: [f32; 3],
    pub mesh: Option<String>,
    pub material_name: Option<String>,
    pub tags: Vec<String>,
    pub animation: Option<AnimationView>,
    pub morph_weights: Option<Vec<f32>>,
    pub visibility: Option<bool>,
    pub casts_shadow: bool,
    pub light: Option<Light>,
    pub camera: Option<Camera>,
    pub is_active_camera: bool,
    pub rigid_body: Option<RigidBodyComponent>,
    pub collider: Option<ColliderComponent>,
    pub character_controller: Option<CharacterControllerComponent>,
    pub navmesh_agent: Option<NavMeshAgent>,
    pub particle_emitter: Option<Box<ParticleEmitter>>,
    pub decal: Option<Decal>,
    pub audio_source: Option<AudioSource>,
    pub render_layer: Option<RenderLayer>,
    pub culling_mask: Option<CullingMask>,
    pub camera_culling_mask: Option<CameraCullingMask>,
    pub ignore_parent_scale: bool,
    pub text: Option<(String, TextProperties)>,
    pub script: Option<String>,
    pub present: Vec<ComponentKind>,
    pub addable: Vec<ComponentKind>,
}

/// Gathers the entity's full editable state. Returns `None` when the entity has
/// no local transform, the floor every other read stands on.
pub fn describe_entity(world: &World, entity: Entity) -> Option<EntityView> {
    let transform = world
        .get::<nightshade::ecs::transform::components::LocalTransform>(entity)
        .copied()?;
    let (roll, pitch, yaw) = quat_to_euler_xyz(&transform.rotation);

    let name = world
        .get::<nightshade::ecs::primitives::Name>(entity)
        .map(|name| name.0.clone())
        .filter(|name| !name.is_empty())
        .unwrap_or_else(|| format!("Entity {}", entity.id));

    let text = world
        .get::<nightshade::ecs::text::components::Text>(entity)
        .map(|text| {
            let content = world
                .resources
                .text
                .cache
                .get_text(text.text_index)
                .map(str::to_string)
                .unwrap_or_default();
            (content, text.properties.clone())
        });

    Some(EntityView {
        id: entity.id,
        name,
        translation: [
            transform.translation.x,
            transform.translation.y,
            transform.translation.z,
        ],
        rotation: [roll.to_degrees(), pitch.to_degrees(), yaw.to_degrees()],
        scale: [transform.scale.x, transform.scale.y, transform.scale.z],
        mesh: world
            .get::<nightshade::ecs::mesh::components::RenderMesh>(entity)
            .map(|mesh| mesh.name.clone()),
        material_name: world
            .get::<nightshade::ecs::material::components::MaterialRef>(entity)
            .map(|reference| reference.name.clone()),
        tags: world
            .resources
            .entities
            .tags
            .get(&entity)
            .cloned()
            .unwrap_or_default(),
        animation: world
            .get::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
            .map(animation_view),
        morph_weights: world
            .get::<nightshade::ecs::morph::components::MorphWeights>(entity)
            .map(|weights| weights.weights.clone()),
        visibility: world
            .get::<nightshade::ecs::primitives::Visibility>(entity)
            .map(|visibility| visibility.visible),
        casts_shadow: world.has::<nightshade::ecs::primitives::CastsShadow>(entity),
        light: world
            .get::<nightshade::ecs::light::components::Light>(entity)
            .cloned(),
        camera: world
            .get::<nightshade::ecs::camera::components::Camera>(entity)
            .copied(),
        is_active_camera: world.resources.active_camera == Some(entity),
        rigid_body: world
            .get::<nightshade::plugins::physics::components::RigidBodyComponent>(entity)
            .cloned(),
        collider: world
            .get::<nightshade::plugins::physics::components::ColliderComponent>(entity)
            .cloned(),
        character_controller: world
            .get::<nightshade::plugins::physics::components::CharacterControllerComponent>(entity)
            .cloned(),
        navmesh_agent: world
            .get::<nightshade::plugins::navmesh::components::NavMeshAgent>(entity)
            .cloned(),
        particle_emitter: world
            .get::<nightshade::render::particles::ParticleEmitter>(entity)
            .cloned()
            .map(Box::new),
        decal: world
            .get::<nightshade::ecs::decal::components::Decal>(entity)
            .cloned(),
        audio_source: world
            .get::<nightshade::plugins::audio::components::AudioSource>(entity)
            .cloned(),
        render_layer: world
            .get::<nightshade::render::render_layer::RenderLayer>(entity)
            .copied(),
        culling_mask: world
            .get::<nightshade::ecs::primitives::CullingMask>(entity)
            .copied(),
        camera_culling_mask: world
            .get::<nightshade::ecs::primitives::CameraCullingMask>(entity)
            .copied(),
        ignore_parent_scale: world
            .has::<nightshade::ecs::transform::components::IgnoreParentScale>(entity),
        text,
        script: world
            .get::<nightshade::ecs::script::components::Script>(entity)
            .map(|script| script.source_text().to_string()),
        present: present_components(world, entity),
        addable: addable_components(world, entity),
    })
}