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.resources.assets.material_registry.registry,
65        &material_ref.name,
66    )
67    .cloned()
68}
69
70/// The entity's base color, the read side of
71/// [`set_color`](crate::prelude::set_color).
72pub fn color(world: &World, entity: Entity) -> Option<[f32; 4]> {
73    material_of(world, entity).map(|material| material.base_color)
74}
75
76/// The entity's metallic and roughness factors.
77pub fn metallic_roughness(world: &World, entity: Entity) -> Option<(f32, f32)> {
78    material_of(world, entity).map(|material| (material.metallic, material.roughness))
79}
80
81/// The entity's emissive color and strength.
82pub fn emissive(world: &World, entity: Entity) -> Option<([f32; 3], f32)> {
83    material_of(world, entity)
84        .map(|material| (material.emissive_factor, material.emissive_strength))
85}
86
87/// Whether the entity's material renders unlit.
88pub fn unlit(world: &World, entity: Entity) -> Option<bool> {
89    material_of(world, entity).map(|material| material.unlit)
90}
91
92/// The name of the entity's base color texture, if it has one.
93pub fn texture(world: &World, entity: Entity) -> Option<String> {
94    material_of(world, entity).and_then(|material| material.base_texture)
95}
96
97/// The entity's animation player state, summarized for an inspector.
98#[derive(Clone, PartialEq, Serialize, Deserialize)]
99pub struct AnimationView {
100    pub clips: Vec<String>,
101    pub current: Option<u32>,
102    pub playing: bool,
103    pub time: f32,
104    pub duration: f32,
105    pub speed: f32,
106    pub looping: bool,
107}
108
109/// Reads an entity's animation player into an [`AnimationView`].
110pub fn animation_view(player: &AnimationPlayer) -> AnimationView {
111    let duration = player
112        .get_current_clip()
113        .map(|clip| clip.duration)
114        .unwrap_or(0.0);
115    AnimationView {
116        clips: player.clips.iter().map(|clip| clip.name.clone()).collect(),
117        current: player.current_clip.map(|index| index as u32),
118        playing: player.playing,
119        time: player.time,
120        duration,
121        speed: player.speed,
122        looping: player.looping,
123    }
124}
125
126/// An entity's whole editable state in one value: transform as euler degrees,
127/// looks, every optional component it carries, and the lists of which
128/// components it has and could gain. The mirror of the spawn-and-set API,
129/// gathered for an inspector or a binding to render.
130#[derive(Clone, Serialize, Deserialize)]
131pub struct EntityView {
132    pub id: u32,
133    pub name: String,
134    pub translation: [f32; 3],
135    pub rotation: [f32; 3],
136    pub scale: [f32; 3],
137    pub mesh: Option<String>,
138    pub material_name: Option<String>,
139    pub tags: Vec<String>,
140    pub animation: Option<AnimationView>,
141    pub morph_weights: Option<Vec<f32>>,
142    pub visibility: Option<bool>,
143    pub casts_shadow: bool,
144    pub light: Option<Light>,
145    pub camera: Option<Camera>,
146    pub is_active_camera: bool,
147    pub rigid_body: Option<RigidBodyComponent>,
148    pub collider: Option<ColliderComponent>,
149    pub character_controller: Option<CharacterControllerComponent>,
150    pub navmesh_agent: Option<NavMeshAgent>,
151    pub particle_emitter: Option<Box<ParticleEmitter>>,
152    pub decal: Option<Decal>,
153    pub audio_source: Option<AudioSource>,
154    pub render_layer: Option<RenderLayer>,
155    pub culling_mask: Option<CullingMask>,
156    pub camera_culling_mask: Option<CameraCullingMask>,
157    pub ignore_parent_scale: bool,
158    pub text: Option<(String, TextProperties)>,
159    pub script: Option<String>,
160    pub present: Vec<ComponentKind>,
161    pub addable: Vec<ComponentKind>,
162}
163
164/// Gathers the entity's full editable state. Returns `None` when the entity has
165/// no local transform, the floor every other read stands on.
166pub fn describe_entity(world: &World, entity: Entity) -> Option<EntityView> {
167    let transform = world
168        .get::<nightshade::ecs::transform::components::LocalTransform>(entity)
169        .copied()?;
170    let (roll, pitch, yaw) = quat_to_euler_xyz(&transform.rotation);
171
172    let name = world
173        .get::<nightshade::ecs::primitives::Name>(entity)
174        .map(|name| name.0.clone())
175        .filter(|name| !name.is_empty())
176        .unwrap_or_else(|| format!("Entity {}", entity.id));
177
178    let text = world
179        .get::<nightshade::ecs::text::components::Text>(entity)
180        .map(|text| {
181            let content = world
182                .resources
183                .text
184                .cache
185                .get_text(text.text_index)
186                .map(str::to_string)
187                .unwrap_or_default();
188            (content, text.properties.clone())
189        });
190
191    Some(EntityView {
192        id: entity.id,
193        name,
194        translation: [
195            transform.translation.x,
196            transform.translation.y,
197            transform.translation.z,
198        ],
199        rotation: [roll.to_degrees(), pitch.to_degrees(), yaw.to_degrees()],
200        scale: [transform.scale.x, transform.scale.y, transform.scale.z],
201        mesh: world
202            .get::<nightshade::ecs::mesh::components::RenderMesh>(entity)
203            .map(|mesh| mesh.name.clone()),
204        material_name: world
205            .get::<nightshade::ecs::material::components::MaterialRef>(entity)
206            .map(|reference| reference.name.clone()),
207        tags: world
208            .resources
209            .entities
210            .tags
211            .get(&entity)
212            .cloned()
213            .unwrap_or_default(),
214        animation: world
215            .get::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
216            .map(animation_view),
217        morph_weights: world
218            .get::<nightshade::ecs::morph::components::MorphWeights>(entity)
219            .map(|weights| weights.weights.clone()),
220        visibility: world
221            .get::<nightshade::ecs::primitives::Visibility>(entity)
222            .map(|visibility| visibility.visible),
223        casts_shadow: world.has::<nightshade::ecs::primitives::CastsShadow>(entity),
224        light: world
225            .get::<nightshade::ecs::light::components::Light>(entity)
226            .cloned(),
227        camera: world
228            .get::<nightshade::ecs::camera::components::Camera>(entity)
229            .copied(),
230        is_active_camera: world.resources.active_camera == Some(entity),
231        rigid_body: world
232            .get::<nightshade::plugins::physics::components::RigidBodyComponent>(entity)
233            .cloned(),
234        collider: world
235            .get::<nightshade::plugins::physics::components::ColliderComponent>(entity)
236            .cloned(),
237        character_controller: world
238            .get::<nightshade::plugins::physics::components::CharacterControllerComponent>(entity)
239            .cloned(),
240        navmesh_agent: world
241            .get::<nightshade::plugins::navmesh::components::NavMeshAgent>(entity)
242            .cloned(),
243        particle_emitter: world
244            .get::<nightshade::render::particles::ParticleEmitter>(entity)
245            .cloned()
246            .map(Box::new),
247        decal: world
248            .get::<nightshade::ecs::decal::components::Decal>(entity)
249            .cloned(),
250        audio_source: world
251            .get::<nightshade::plugins::audio::components::AudioSource>(entity)
252            .cloned(),
253        render_layer: world
254            .get::<nightshade::render::render_layer::RenderLayer>(entity)
255            .copied(),
256        culling_mask: world
257            .get::<nightshade::ecs::primitives::CullingMask>(entity)
258            .copied(),
259        camera_culling_mask: world
260            .get::<nightshade::ecs::primitives::CameraCullingMask>(entity)
261            .copied(),
262        ignore_parent_scale: world
263            .has::<nightshade::ecs::transform::components::IgnoreParentScale>(entity),
264        text,
265        script: world
266            .get::<nightshade::ecs::script::components::Script>(entity)
267            .map(|script| script.source_text().to_string()),
268        present: present_components(world, entity),
269        addable: addable_components(world, entity),
270    })
271}