nightshade-api 0.48.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::audio::components::AudioSource;
use nightshade::ecs::camera::components::Camera;
use nightshade::ecs::decal::components::Decal;
use nightshade::ecs::material::components::Material;
use nightshade::ecs::particles::components::ParticleEmitter;
use nightshade::ecs::primitives::{CameraCullingMask, CullingMask, RenderLayer};
use nightshade::ecs::text::components::TextProperties;
use nightshade::prelude::*;
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.core.get_material_ref(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 get_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 get_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 get_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 get_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 get_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.core.get_local_transform(entity).copied()?;
    let (roll, pitch, yaw) = quat_to_euler_xyz(&transform.rotation);

    let name = world
        .core
        .get_name(entity)
        .map(|name| name.0.clone())
        .filter(|name| !name.is_empty())
        .unwrap_or_else(|| format!("Entity {}", entity.id));

    let text = world.core.get_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
            .core
            .get_render_mesh(entity)
            .map(|mesh| mesh.name.clone()),
        material_name: world
            .core
            .get_material_ref(entity)
            .map(|reference| reference.name.clone()),
        tags: world
            .resources
            .entities
            .tags
            .get(&entity)
            .cloned()
            .unwrap_or_default(),
        animation: world.core.get_animation_player(entity).map(animation_view),
        morph_weights: world
            .core
            .get_morph_weights(entity)
            .map(|weights| weights.weights.clone()),
        visibility: world
            .core
            .get_visibility(entity)
            .map(|visibility| visibility.visible),
        casts_shadow: world.core.entity_has_casts_shadow(entity),
        light: world.core.get_light(entity).cloned(),
        camera: world.core.get_camera(entity).copied(),
        is_active_camera: world.resources.active_camera == Some(entity),
        rigid_body: world.core.get_rigid_body(entity).cloned(),
        collider: world.core.get_collider(entity).cloned(),
        character_controller: world.core.get_character_controller(entity).cloned(),
        navmesh_agent: world.core.get_navmesh_agent(entity).cloned(),
        particle_emitter: world
            .core
            .get_particle_emitter(entity)
            .cloned()
            .map(Box::new),
        decal: world.core.get_decal(entity).cloned(),
        audio_source: world.core.get_audio_source(entity).cloned(),
        render_layer: world.core.get_render_layer(entity).copied(),
        culling_mask: world.core.get_culling_mask(entity).copied(),
        camera_culling_mask: world.core.get_camera_culling_mask(entity).copied(),
        ignore_parent_scale: world.core.entity_has_ignore_parent_scale(entity),
        text,
        script: world
            .core
            .get_script(entity)
            .map(|script| script.source_text().to_string()),
        present: present_components(world, entity),
        addable: addable_components(world, entity),
    })
}