Skip to main content

nightshade_api/
inspect.rs

1//! Reading back what the setters write: per-entity material getters, the
2//! quaternion/euler conversions an inspector needs, and [`describe_entity`],
3//! which gathers an entity's whole editable state into one [`EntityView`].
4
5use crate::reflect::{ComponentKind, addable_components, present_components};
6use nightshade::ecs::animation::components::AnimationPlayer;
7use nightshade::ecs::audio::components::AudioSource;
8use nightshade::ecs::camera::components::Camera;
9use nightshade::ecs::decal::components::Decal;
10use nightshade::ecs::material::components::Material;
11use nightshade::ecs::particles::components::ParticleEmitter;
12use nightshade::ecs::primitives::{CameraCullingMask, CullingMask, RenderLayer};
13use nightshade::ecs::text::components::TextProperties;
14use nightshade::prelude::*;
15use serde::{Deserialize, Serialize};
16
17/// Converts a rotation quaternion to intrinsic XYZ euler angles in radians, the
18/// roll/pitch/yaw an inspector edits.
19pub fn quat_to_euler_xyz(quat: &Quat) -> (f32, f32, f32) {
20    let sinr = 2.0 * (quat.w * quat.i + quat.j * quat.k);
21    let cosr = 1.0 - 2.0 * (quat.i * quat.i + quat.j * quat.j);
22    let roll = sinr.atan2(cosr);
23
24    let sinp = 2.0 * (quat.w * quat.j - quat.k * quat.i);
25    let pitch = if sinp.abs() >= 1.0 {
26        std::f32::consts::FRAC_PI_2.copysign(sinp)
27    } else {
28        sinp.asin()
29    };
30
31    let siny = 2.0 * (quat.w * quat.k + quat.i * quat.j);
32    let cosy = 1.0 - 2.0 * (quat.j * quat.j + quat.k * quat.k);
33    let yaw = siny.atan2(cosy);
34
35    (roll, pitch, yaw)
36}
37
38/// Builds a rotation quaternion from intrinsic XYZ euler angles in radians, the
39/// inverse of [`quat_to_euler_xyz`].
40pub fn euler_xyz_to_quat(roll: f32, pitch: f32, yaw: f32) -> Quat {
41    let cr = (roll * 0.5).cos();
42    let sr = (roll * 0.5).sin();
43    let cp = (pitch * 0.5).cos();
44    let sp = (pitch * 0.5).sin();
45    let cy = (yaw * 0.5).cos();
46    let sy = (yaw * 0.5).sin();
47
48    Quat::from_parts(
49        cr * cp * cy + sr * sp * sy,
50        nalgebra_glm::Vec3::new(
51            sr * cp * cy - cr * sp * sy,
52            cr * sp * cy + sr * cp * sy,
53            cr * cp * sy - sr * sp * cy,
54        ),
55    )
56}
57
58/// The entity's material, if it has one. A clone of the registry entry its
59/// `MaterialRef` names, so reads do not borrow the registry.
60pub fn material_of(world: &World, entity: Entity) -> Option<Material> {
61    let material_ref = world.core.get_material_ref(entity)?;
62    registry_entry_by_name(
63        &world.resources.assets.material_registry.registry,
64        &material_ref.name,
65    )
66    .cloned()
67}
68
69/// The entity's base color, the read side of
70/// [`set_color`](crate::prelude::set_color).
71pub fn get_color(world: &World, entity: Entity) -> Option<[f32; 4]> {
72    material_of(world, entity).map(|material| material.base_color)
73}
74
75/// The entity's metallic and roughness factors.
76pub fn get_metallic_roughness(world: &World, entity: Entity) -> Option<(f32, f32)> {
77    material_of(world, entity).map(|material| (material.metallic, material.roughness))
78}
79
80/// The entity's emissive color and strength.
81pub fn get_emissive(world: &World, entity: Entity) -> Option<([f32; 3], f32)> {
82    material_of(world, entity)
83        .map(|material| (material.emissive_factor, material.emissive_strength))
84}
85
86/// Whether the entity's material renders unlit.
87pub fn get_unlit(world: &World, entity: Entity) -> Option<bool> {
88    material_of(world, entity).map(|material| material.unlit)
89}
90
91/// The name of the entity's base color texture, if it has one.
92pub fn get_texture(world: &World, entity: Entity) -> Option<String> {
93    material_of(world, entity).and_then(|material| material.base_texture)
94}
95
96/// The entity's animation player state, summarized for an inspector.
97#[derive(Clone, PartialEq, Serialize, Deserialize)]
98pub struct AnimationView {
99    pub clips: Vec<String>,
100    pub current: Option<u32>,
101    pub playing: bool,
102    pub time: f32,
103    pub duration: f32,
104    pub speed: f32,
105    pub looping: bool,
106}
107
108/// Reads an entity's animation player into an [`AnimationView`].
109pub fn animation_view(player: &AnimationPlayer) -> AnimationView {
110    let duration = player
111        .get_current_clip()
112        .map(|clip| clip.duration)
113        .unwrap_or(0.0);
114    AnimationView {
115        clips: player.clips.iter().map(|clip| clip.name.clone()).collect(),
116        current: player.current_clip.map(|index| index as u32),
117        playing: player.playing,
118        time: player.time,
119        duration,
120        speed: player.speed,
121        looping: player.looping,
122    }
123}
124
125/// An entity's whole editable state in one value: transform as euler degrees,
126/// looks, every optional component it carries, and the lists of which
127/// components it has and could gain. The mirror of the spawn-and-set API,
128/// gathered for an inspector or a binding to render.
129#[derive(Clone, Serialize, Deserialize)]
130pub struct EntityView {
131    pub id: u32,
132    pub name: String,
133    pub translation: [f32; 3],
134    pub rotation: [f32; 3],
135    pub scale: [f32; 3],
136    pub mesh: Option<String>,
137    pub material_name: Option<String>,
138    pub tags: Vec<String>,
139    pub animation: Option<AnimationView>,
140    pub morph_weights: Option<Vec<f32>>,
141    pub visibility: Option<bool>,
142    pub casts_shadow: bool,
143    pub light: Option<Light>,
144    pub camera: Option<Camera>,
145    pub is_active_camera: bool,
146    pub rigid_body: Option<RigidBodyComponent>,
147    pub collider: Option<ColliderComponent>,
148    pub character_controller: Option<CharacterControllerComponent>,
149    pub navmesh_agent: Option<NavMeshAgent>,
150    pub particle_emitter: Option<Box<ParticleEmitter>>,
151    pub decal: Option<Decal>,
152    pub audio_source: Option<AudioSource>,
153    pub render_layer: Option<RenderLayer>,
154    pub culling_mask: Option<CullingMask>,
155    pub camera_culling_mask: Option<CameraCullingMask>,
156    pub ignore_parent_scale: bool,
157    pub text: Option<(String, TextProperties)>,
158    pub script: Option<String>,
159    pub present: Vec<ComponentKind>,
160    pub addable: Vec<ComponentKind>,
161}
162
163/// Gathers the entity's full editable state. Returns `None` when the entity has
164/// no local transform, the floor every other read stands on.
165pub fn describe_entity(world: &World, entity: Entity) -> Option<EntityView> {
166    let transform = world.core.get_local_transform(entity).copied()?;
167    let (roll, pitch, yaw) = quat_to_euler_xyz(&transform.rotation);
168
169    let name = world
170        .core
171        .get_name(entity)
172        .map(|name| name.0.clone())
173        .filter(|name| !name.is_empty())
174        .unwrap_or_else(|| format!("Entity {}", entity.id));
175
176    let text = world.core.get_text(entity).map(|text| {
177        let content = world
178            .resources
179            .text
180            .cache
181            .get_text(text.text_index)
182            .map(str::to_string)
183            .unwrap_or_default();
184        (content, text.properties.clone())
185    });
186
187    Some(EntityView {
188        id: entity.id,
189        name,
190        translation: [
191            transform.translation.x,
192            transform.translation.y,
193            transform.translation.z,
194        ],
195        rotation: [roll.to_degrees(), pitch.to_degrees(), yaw.to_degrees()],
196        scale: [transform.scale.x, transform.scale.y, transform.scale.z],
197        mesh: world
198            .core
199            .get_render_mesh(entity)
200            .map(|mesh| mesh.name.clone()),
201        material_name: world
202            .core
203            .get_material_ref(entity)
204            .map(|reference| reference.name.clone()),
205        tags: world
206            .resources
207            .entities
208            .tags
209            .get(&entity)
210            .cloned()
211            .unwrap_or_default(),
212        animation: world.core.get_animation_player(entity).map(animation_view),
213        morph_weights: world
214            .core
215            .get_morph_weights(entity)
216            .map(|weights| weights.weights.clone()),
217        visibility: world
218            .core
219            .get_visibility(entity)
220            .map(|visibility| visibility.visible),
221        casts_shadow: world.core.entity_has_casts_shadow(entity),
222        light: world.core.get_light(entity).cloned(),
223        camera: world.core.get_camera(entity).copied(),
224        is_active_camera: world.resources.active_camera == Some(entity),
225        rigid_body: world.core.get_rigid_body(entity).cloned(),
226        collider: world.core.get_collider(entity).cloned(),
227        character_controller: world.core.get_character_controller(entity).cloned(),
228        navmesh_agent: world.core.get_navmesh_agent(entity).cloned(),
229        particle_emitter: world
230            .core
231            .get_particle_emitter(entity)
232            .cloned()
233            .map(Box::new),
234        decal: world.core.get_decal(entity).cloned(),
235        audio_source: world.core.get_audio_source(entity).cloned(),
236        render_layer: world.core.get_render_layer(entity).copied(),
237        culling_mask: world.core.get_culling_mask(entity).copied(),
238        camera_culling_mask: world.core.get_camera_culling_mask(entity).copied(),
239        ignore_parent_scale: world.core.entity_has_ignore_parent_scale(entity),
240        text,
241        script: world
242            .core
243            .get_script(entity)
244            .map(|script| script.source_text().to_string()),
245        present: present_components(world, entity),
246        addable: addable_components(world, entity),
247    })
248}