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