Skip to main content

nightshade_api/
scene.rs

1//! Retained scene content: primitives, models, hierarchy, and the [`Object`]
2//! descriptor for spawning shape, color, and physics in one call.
3
4use crate::palette::WHITE;
5use crate::runner::MATERIAL_PREFIX;
6use nightshade::prelude::nalgebra_glm::Mat4;
7use nightshade::prelude::*;
8use serde::{Deserialize, Serialize};
9
10pub use nightshade::prelude::despawn_recursive_immediate as despawn;
11pub use nightshade::prelude::spawn_cone_at as spawn_cone;
12pub use nightshade::prelude::spawn_cube_at as spawn_cube;
13pub use nightshade::prelude::spawn_cylinder_at as spawn_cylinder;
14pub use nightshade::prelude::spawn_plane_at as spawn_plane;
15pub use nightshade::prelude::spawn_sphere_at as spawn_sphere;
16pub use nightshade::prelude::spawn_torus_at as spawn_torus;
17
18/// The primitive shapes [`spawn_object`] can produce.
19#[derive(
20    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, enum2schema::Schema,
21)]
22pub enum Shape {
23    #[default]
24    Cube,
25    Sphere,
26    Cylinder,
27    Cone,
28    Torus,
29    Plane,
30}
31
32/// Physics participation for [`spawn_object`]. Requires the `physics`
33/// feature, which is on by default.
34///
35/// Dynamic cubes, spheres, cylinders, and cones get exact colliders. A
36/// dynamic torus gets a convex hull of its mesh, which fills the hole. Static
37/// toruses and planes collide against the exact triangle mesh.
38#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, enum2schema::Schema)]
39pub enum Body {
40    #[default]
41    None,
42    Static,
43    Dynamic {
44        mass: f32,
45    },
46}
47
48/// Everything about one object in a single struct literal.
49///
50/// ```ignore
51/// let ball = spawn_object(world, Object {
52///     shape: Shape::Sphere,
53///     position: vec3(0.0, 4.0, 0.0),
54///     color: RED,
55///     body: Body::Dynamic { mass: 2.0 },
56///     ..Object::default()
57/// });
58/// ```
59pub struct Object {
60    pub shape: Shape,
61    pub position: Vec3,
62    pub scale: Vec3,
63    pub color: [f32; 4],
64    pub body: Body,
65}
66
67impl Default for Object {
68    fn default() -> Self {
69        Self {
70            shape: Shape::Cube,
71            position: Vec3::zeros(),
72            scale: Vec3::new(1.0, 1.0, 1.0),
73            color: WHITE,
74            body: Body::None,
75        }
76    }
77}
78
79/// Spawns an [`Object`]: mesh, color, and optional physics body in one call.
80pub fn spawn_object(world: &mut World, object: Object) -> Entity {
81    let entity = spawn_mesh_at(
82        world,
83        mesh_name(object.shape),
84        object.position,
85        object.scale,
86    );
87    crate::appearance::set_color(world, entity, object.color);
88    match object.body {
89        Body::None => {}
90        #[cfg(feature = "physics")]
91        Body::Static => {
92            let collider = static_collider(world, object.shape, object.scale)
93                .with_friction(0.8)
94                .with_restitution(0.1);
95            attach_body(
96                world,
97                entity,
98                RigidBodyComponent::new_static().with_translation(
99                    object.position.x,
100                    object.position.y,
101                    object.position.z,
102                ),
103                collider,
104                false,
105            );
106        }
107        #[cfg(feature = "physics")]
108        Body::Dynamic { mass } => {
109            let collider = dynamic_collider(world, object.shape, object.scale)
110                .with_friction(0.7)
111                .with_restitution(0.2);
112            attach_body(
113                world,
114                entity,
115                RigidBodyComponent::new_dynamic()
116                    .with_translation(object.position.x, object.position.y, object.position.z)
117                    .with_mass(mass),
118                collider,
119                true,
120            );
121        }
122        #[cfg(not(feature = "physics"))]
123        Body::Static | Body::Dynamic { .. } => {}
124    }
125    entity
126}
127
128/// Spawns a dynamic capsule physics body at `position`: a `radius` pill with a
129/// `half_height` cylindrical section, `mass`, and a linear RGBA `color`. The
130/// capsule is the upright character shape. The visual is a cylinder, the
131/// collider is the true capsule. Capsule is not a [`Shape`], so it has its own
132/// spawn rather than coming through [`spawn_object`].
133#[cfg(feature = "physics")]
134pub fn spawn_capsule_body(
135    world: &mut World,
136    position: Vec3,
137    half_height: f32,
138    radius: f32,
139    mass: f32,
140    color: [f32; 4],
141) -> Entity {
142    let scale = Vec3::new(radius * 2.0, (half_height + radius) * 2.0, radius * 2.0);
143    let entity = spawn_mesh_at(world, mesh_name(Shape::Cylinder), position, scale);
144    crate::appearance::set_color(world, entity, color);
145    let collider = ColliderComponent::new_capsule(half_height, radius)
146        .with_friction(0.7)
147        .with_restitution(0.2);
148    attach_body(
149        world,
150        entity,
151        RigidBodyComponent::new_dynamic()
152            .with_translation(position.x, position.y, position.z)
153            .with_mass(mass),
154        collider,
155        true,
156    );
157    entity
158}
159
160/// Spawns a static, collidable mesh from raw geometry at `position`: it renders
161/// the mesh and gives it an exact triangle collider, so dynamic bodies collide
162/// with arbitrary level geometry. Each vertex is a (position, normal, uv) triple
163/// and `indices` lists triangle corners three at a time. Heavy, so use it for
164/// static level geometry, not movers.
165#[cfg(feature = "physics")]
166pub fn spawn_custom_mesh_collider(
167    world: &mut World,
168    name: &str,
169    vertices: &[([f32; 3], [f32; 3], [f32; 2])],
170    indices: &[u32],
171    position: Vec3,
172) -> Entity {
173    let entity = crate::mesh::spawn_custom_mesh(world, name, vertices, indices, position);
174    let positions: Vec<[f32; 3]> = vertices.iter().map(|(point, _, _)| *point).collect();
175    let triangles: Vec<[u32; 3]> = indices
176        .chunks_exact(3)
177        .map(|chunk| [chunk[0], chunk[1], chunk[2]])
178        .collect();
179    let collider = ColliderComponent {
180        shape: nightshade::plugins::physics::components::ColliderShape::TriMesh {
181            vertices: positions,
182            indices: triangles,
183        },
184        ..Default::default()
185    };
186    attach_body(
187        world,
188        entity,
189        RigidBodyComponent::new_static().with_translation(position.x, position.y, position.z),
190        collider,
191        false,
192    );
193    entity
194}
195
196/// Spawns one [`Object`] at each position, all sharing a single registered
197/// material, which is what writing it longhand for a crowd looks like. A
198/// thousand entities through this hold one material entry, not a thousand.
199pub fn spawn_objects(world: &mut World, object: Object, positions: &[Vec3]) -> Vec<Entity> {
200    let mut entities = Vec::with_capacity(positions.len());
201    let mut shared_material: Option<String> = None;
202    #[cfg(feature = "physics")]
203    let mut collider_template: Option<ColliderComponent> = None;
204
205    for &position in positions {
206        let entity = spawn_mesh_at(world, mesh_name(object.shape), position, object.scale);
207        match shared_material.as_deref() {
208            None => {
209                crate::appearance::set_color(world, entity, object.color);
210                shared_material = world
211                    .get::<nightshade::ecs::material::components::MaterialRef>(entity)
212                    .map(|material_ref| material_ref.name.clone());
213            }
214            Some(name) => {
215                let name = name.to_string();
216                adopt_shared_material(world, entity, &name);
217            }
218        }
219
220        #[cfg(feature = "physics")]
221        match object.body {
222            Body::None => {}
223            Body::Static => {
224                let collider = collider_template
225                    .get_or_insert_with(|| {
226                        static_collider(world, object.shape, object.scale)
227                            .with_friction(0.8)
228                            .with_restitution(0.1)
229                    })
230                    .clone();
231                attach_body(
232                    world,
233                    entity,
234                    RigidBodyComponent::new_static()
235                        .with_translation(position.x, position.y, position.z),
236                    collider,
237                    false,
238                );
239            }
240            Body::Dynamic { mass } => {
241                let collider = collider_template
242                    .get_or_insert_with(|| {
243                        dynamic_collider(world, object.shape, object.scale)
244                            .with_friction(0.7)
245                            .with_restitution(0.2)
246                    })
247                    .clone();
248                attach_body(
249                    world,
250                    entity,
251                    RigidBodyComponent::new_dynamic()
252                        .with_translation(position.x, position.y, position.z)
253                        .with_mass(mass),
254                    collider,
255                    true,
256                );
257            }
258        }
259
260        entities.push(entity);
261    }
262    entities
263}
264
265/// Spawns one entity that renders `transforms.len()` copies of the shape in a
266/// single draw call, the cheapest way to put thousands of identical things on
267/// screen. Per copy control afterward goes through the entity's
268/// `InstancedMesh` component.
269pub fn spawn_instanced(
270    world: &mut World,
271    shape: Shape,
272    transforms: Vec<InstanceTransform>,
273    color: [f32; 4],
274) -> Entity {
275    let shape_mesh_name = mesh_name(shape);
276    ensure_primitive_mesh(world, shape_mesh_name);
277    let fallback = format!(
278        "api::material::instanced::{:.4}_{:.4}_{:.4}_{:.4}",
279        color[0], color[1], color[2], color[3]
280    );
281    let material_name = nightshade::ecs::material::resources::material_registry_find_or_insert(
282        &mut world
283            .res_mut::<nightshade::ecs::asset_state::AssetState>()
284            .material_registry,
285        fallback,
286        Material {
287            base_color: color,
288            ..Default::default()
289        },
290    );
291    spawn_instanced_mesh_with_material(world, shape_mesh_name, transforms, &material_name)
292}
293
294/// Spawns an instanced mesh of `shape` that draws every transform in one batch
295/// using the registry material named `material`, for large crowds sharing a
296/// custom look. Register the material first with
297/// [`register_material`](crate::prelude::register_material). Returns the batch
298/// entity; restream its transforms with [`set_instances`].
299pub fn spawn_instanced_with_material(
300    world: &mut World,
301    shape: Shape,
302    transforms: Vec<InstanceTransform>,
303    material: &str,
304) -> Entity {
305    let shape_mesh_name = mesh_name(shape);
306    ensure_primitive_mesh(world, shape_mesh_name);
307    spawn_instanced_mesh_with_material(world, shape_mesh_name, transforms, material)
308}
309
310/// Replaces an instanced batch's transforms and flags the renderer to reupload
311/// its instance buffer, for streaming worlds and animated instance sets that
312/// change every frame.
313pub fn set_instances(world: &mut World, batch: Entity, transforms: Vec<InstanceTransform>) {
314    if let Some(instanced) =
315        world.get_mut::<nightshade::ecs::mesh::components::InstancedMesh>(batch)
316    {
317        instanced.set_instances(transforms);
318    }
319    world
320        .res_mut::<nightshade::render::mesh_state::MeshRenderState>()
321        .mark_instanced_meshes_changed();
322}
323
324fn adopt_shared_material(world: &mut World, entity: Entity, name: &str) {
325    let previous = world
326        .get::<nightshade::ecs::material::components::MaterialRef>(entity)
327        .map(|material_ref| material_ref.name.clone());
328    if let Some(previous_name) = previous
329        && let Some((index, _)) = registry_lookup_index(
330            &world
331                .res::<nightshade::ecs::asset_state::AssetState>()
332                .material_registry
333                .registry,
334            &previous_name,
335        )
336    {
337        registry_remove_reference(
338            &mut world
339                .res_mut::<nightshade::ecs::asset_state::AssetState>()
340                .material_registry
341                .registry,
342            index,
343        );
344    }
345    if let Some((index, _)) = registry_lookup_index(
346        &world
347            .res::<nightshade::ecs::asset_state::AssetState>()
348            .material_registry
349            .registry,
350        name,
351    ) {
352        registry_add_reference(
353            &mut world
354                .res_mut::<nightshade::ecs::asset_state::AssetState>()
355                .material_registry
356                .registry,
357            index,
358        );
359    }
360    world.set(entity, MaterialRef::new(name.to_string()));
361    world
362        .res_mut::<nightshade::render::mesh_state::MeshRenderState>()
363        .mark_entity_added(render_entity(entity));
364}
365
366pub(crate) fn ensure_primitive_mesh(world: &mut World, mesh_name: &str) {
367    use nightshade::render::procedural_meshes::{
368        create_cone_mesh, create_cube_mesh, create_cylinder_mesh, create_plane_mesh,
369        create_sphere_mesh, create_torus_mesh,
370    };
371    if !world
372        .res::<nightshade::ecs::asset_state::AssetState>()
373        .mesh_cache
374        .registry
375        .name_to_index
376        .contains_key(mesh_name)
377    {
378        let mesh = match mesh_name {
379            "Cube" => create_cube_mesh(),
380            "Sphere" => create_sphere_mesh(1.0, 16),
381            "Plane" => create_plane_mesh(2.0),
382            "Torus" => create_torus_mesh(1.0, 0.3, 32, 16),
383            "Cylinder" => create_cylinder_mesh(0.5, 1.0, 16),
384            _ => create_cone_mesh(0.5, 1.0, 16),
385        };
386        mesh_cache_insert(
387            &mut world
388                .res_mut::<nightshade::ecs::asset_state::AssetState>()
389                .mesh_cache,
390            mesh_name.to_string(),
391            mesh,
392        );
393    }
394    if let Some((index, _)) = registry_lookup_index(
395        &world
396            .res::<nightshade::ecs::asset_state::AssetState>()
397            .mesh_cache
398            .registry,
399        mesh_name,
400    ) {
401        registry_add_reference(
402            &mut world
403                .res_mut::<nightshade::ecs::asset_state::AssetState>()
404                .mesh_cache
405                .registry,
406            index,
407        );
408    }
409}
410
411/// Spawns a simulated cloth sheet hanging from `position`, pinned along its
412/// top row. It drapes, collides with the ground, and responds to the global
413/// wind resource (`world.res::<nightshade::render::wind::Wind>()`).
414/// Color it like anything else with [`set_color`](crate::prelude::set_color).
415pub fn spawn_cloth_sheet(world: &mut World, position: Vec3, width: f32, height: f32) -> Entity {
416    spawn_cloth(
417        world,
418        Cloth {
419            width,
420            height,
421            ..Default::default()
422        },
423        position,
424        "Cloth".to_string(),
425    )
426}
427
428/// Shows or hides the entity without despawning it.
429pub fn set_visible(world: &mut World, entity: Entity, visible: bool) {
430    if let Some(visibility) = world.get_mut::<nightshade::ecs::primitives::Visibility>(entity) {
431        visibility.visible = visible;
432    }
433}
434
435/// Spawns a flat ground plane reaching `half_extent` in each direction. With
436/// the `physics` feature it carries a static collider so dynamic objects land
437/// on it.
438pub fn spawn_floor(world: &mut World, half_extent: f32) -> Entity {
439    let entity = spawn_mesh_at(
440        world,
441        "Plane",
442        Vec3::zeros(),
443        Vec3::new(half_extent, 1.0, half_extent),
444    );
445    #[cfg(feature = "physics")]
446    attach_body(
447        world,
448        entity,
449        RigidBodyComponent::new_static().with_translation(0.0, -0.05, 0.0),
450        ColliderComponent::new_cuboid(half_extent, 0.05, half_extent)
451            .with_friction(0.8)
452            .with_restitution(0.1),
453        false,
454    );
455    entity
456}
457
458/// Spawns a glb model with its textures, materials, skins, and animations.
459/// Panics with the import error when the bytes are not a valid glb.
460pub fn spawn_model(world: &mut World, glb_bytes: &[u8], position: Vec3) -> Entity {
461    let mut result =
462        import_gltf_from_bytes(glb_bytes).expect("failed to import the glb model bytes");
463    nightshade::ecs::loading::queue_gltf_load(world, &mut result);
464    let prefab = &result.prefabs[0];
465    nightshade::ecs::prefab::commands::spawn_prefab_with_skins(
466        world,
467        prefab,
468        &result.animations,
469        &result.skins,
470        position,
471    )
472}
473
474/// Starts playing the model's animation clip at `clip_index`.
475pub fn play_animation(world: &mut World, entity: Entity, clip_index: usize) {
476    if let Some(player) =
477        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
478    {
479        player.play(clip_index);
480    }
481}
482
483/// Sets whether the entity's current animation repeats.
484pub fn set_animation_looping(world: &mut World, entity: Entity, looping: bool) {
485    if let Some(player) =
486        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
487    {
488        player.looping = looping;
489    }
490}
491
492/// Plays the animation clip named `clip_name`. Returns whether a clip with that
493/// name exists on the model.
494pub fn play_animation_named(world: &mut World, entity: Entity, clip_name: &str) -> bool {
495    if let Some(player) =
496        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
497        && let Some(index) = player.clips.iter().position(|clip| clip.name == clip_name)
498    {
499        player.play(index);
500        return true;
501    }
502    false
503}
504
505/// Sets the playback speed of the entity's animation. 1.0 is normal, 2.0 doubles
506/// it, a negative value plays backward.
507pub fn set_animation_speed(world: &mut World, entity: Entity, speed: f32) {
508    if let Some(player) =
509        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
510    {
511        player.speed = speed;
512    }
513}
514
515/// Cross-fades from the current clip to `clip_index` over `seconds`, a smooth
516/// transition rather than a hard cut.
517pub fn blend_to_animation(world: &mut World, entity: Entity, clip_index: usize, seconds: f32) {
518    if let Some(player) =
519        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
520    {
521        player.blend_to(clip_index, seconds);
522    }
523}
524
525/// Cross-fades to the clip named `clip_name` over `seconds`. Returns whether a
526/// clip with that name exists.
527pub fn blend_to_animation_named(
528    world: &mut World,
529    entity: Entity,
530    clip_name: &str,
531    seconds: f32,
532) -> bool {
533    if let Some(player) =
534        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
535        && let Some(index) = player.clips.iter().position(|clip| clip.name == clip_name)
536    {
537        player.blend_to(index, seconds);
538        return true;
539    }
540    false
541}
542
543/// Pauses the entity's animation at the current frame.
544pub fn pause_animation(world: &mut World, entity: Entity) {
545    if let Some(player) =
546        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
547    {
548        player.pause();
549    }
550}
551
552/// Resumes a paused animation from where it left off.
553pub fn resume_animation(world: &mut World, entity: Entity) {
554    if let Some(player) =
555        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
556    {
557        player.resume();
558    }
559}
560
561/// Stops the entity's animation and resets it to the first frame.
562pub fn stop_animation(world: &mut World, entity: Entity) {
563    if let Some(player) =
564        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
565    {
566        player.stop();
567    }
568}
569
570/// The names of the entity's animation clips, in clip-index order.
571pub fn animation_clips(world: &World, entity: Entity) -> Vec<String> {
572    world
573        .get::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
574        .map(|player| player.clips.iter().map(|clip| clip.name.clone()).collect())
575        .unwrap_or_default()
576}
577
578/// Adds a named marker at `time` seconds on the clip at `clip_index`. While that
579/// clip plays, crossing the marker publishes an `Event::AnimationEvent` carrying
580/// `name`, the cue to spawn a footstep, land a hit, or trigger an effect. Read
581/// it with [`drain_events`](crate::prelude::drain_events). Returns false if the
582/// entity has no animation player or the clip index is out of range.
583pub fn add_animation_event(
584    world: &mut World,
585    entity: Entity,
586    clip_index: usize,
587    time: f32,
588    name: &str,
589) -> bool {
590    if let Some(player) =
591        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
592        && let Some(clip) = player.clips.get_mut(clip_index)
593    {
594        clip.events
595            .push(nightshade::ecs::animation::components::AnimationEvent {
596                time,
597                name: name.to_string(),
598            });
599        return true;
600    }
601    false
602}
603
604/// Adds a named marker at `time` seconds on the clip named `clip_name`, the
605/// by-name form of [`add_animation_event`].
606pub fn add_animation_event_named(
607    world: &mut World,
608    entity: Entity,
609    clip_name: &str,
610    time: f32,
611    name: &str,
612) -> bool {
613    if let Some(player) =
614        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
615        && let Some(clip) = player.clips.iter_mut().find(|clip| clip.name == clip_name)
616    {
617        clip.events
618            .push(nightshade::ecs::animation::components::AnimationEvent {
619                time,
620                name: name.to_string(),
621            });
622        return true;
623    }
624    false
625}
626
627/// Plays an extra clip on top of the base animation at `weight` (0.0 to 1.0),
628/// blended over the bones the clip targets. Returns the layer's index for
629/// [`set_animation_layer_weight`], or `None` if the entity has no animation
630/// player. A reload, a hit reaction, anything composited over a walk or idle.
631pub fn add_animation_layer(
632    world: &mut World,
633    entity: Entity,
634    clip_index: usize,
635    weight: f32,
636) -> Option<usize> {
637    let player =
638        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)?;
639    let index = player.layers.len();
640    player.layers.push(
641        nightshade::ecs::animation::components::AnimationLayer::new(clip_index).with_weight(weight),
642    );
643    Some(index)
644}
645
646/// Plays an extra clip limited to the named bones, the per-bone mask form of
647/// [`add_animation_layer`]. Pass the bone names the layer should drive, like the
648/// spine and arms for an upper-body wave over a full-body walk.
649pub fn add_animation_layer_masked(
650    world: &mut World,
651    entity: Entity,
652    clip_index: usize,
653    weight: f32,
654    bones: &[&str],
655) -> Option<usize> {
656    let player =
657        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)?;
658    let index = player.layers.len();
659    let mask = bones.iter().map(|bone| bone.to_string()).collect();
660    player.layers.push(
661        nightshade::ecs::animation::components::AnimationLayer::new(clip_index)
662            .with_weight(weight)
663            .with_mask(mask),
664    );
665    Some(index)
666}
667
668/// Sets the blend weight of a layer added by [`add_animation_layer`], for fading
669/// a composited animation in and out. 0.0 disables it without removing it.
670pub fn set_animation_layer_weight(
671    world: &mut World,
672    entity: Entity,
673    layer_index: usize,
674    weight: f32,
675) {
676    if let Some(player) =
677        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
678        && let Some(layer) = player.layers.get_mut(layer_index)
679    {
680        layer.weight = weight;
681    }
682}
683
684/// Removes every animation layer, leaving only the base animation.
685pub fn clear_animation_layers(world: &mut World, entity: Entity) {
686    if let Some(player) =
687        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
688    {
689        player.layers.clear();
690    }
691}
692
693/// Registers `entity` under `name` so it can be looked up by name, including
694/// through the `named` map a rhai script reads. Use it to hand specific entities
695/// (a HUD label, a player) to scripts that drive them.
696pub fn name_entity(world: &mut World, name: &str, entity: Entity) {
697    world
698        .res_mut::<nightshade::ecs::entity_registry::EntityRegistry>()
699        .names
700        .insert(name.to_string(), entity);
701}
702
703/// Spawns an invisible group at `position` for building hierarchies. Parent
704/// things to it and move, rotate, or animate the group to drive them all.
705pub fn spawn_group(world: &mut World, position: Vec3) -> Entity {
706    let entity = spawn_entities(world, NAME | LOCAL_TRANSFORM | GLOBAL_TRANSFORM, 1)[0];
707    world.set(entity, Name("Group".to_string()));
708    world.set(
709        entity,
710        LocalTransform {
711            translation: position,
712            ..Default::default()
713        },
714    );
715    entity
716}
717
718/// Parents `child` to `parent`, or unparents it with `None`, keeping the
719/// child exactly where it is in world space.
720pub fn set_parent(world: &mut World, child: Entity, parent: Option<Entity>) {
721    let child_world = crate::placement::world_matrix(world, child);
722    let parent_world = parent
723        .map(|parent_entity| crate::placement::world_matrix(world, parent_entity))
724        .unwrap_or_else(Mat4::identity);
725    let local = nalgebra_glm::inverse(&parent_world) * child_world;
726
727    let translation = nalgebra_glm::vec3(local[(0, 3)], local[(1, 3)], local[(2, 3)]);
728    let basis_x = nalgebra_glm::vec3(local[(0, 0)], local[(1, 0)], local[(2, 0)]);
729    let basis_y = nalgebra_glm::vec3(local[(0, 1)], local[(1, 1)], local[(2, 1)]);
730    let basis_z = nalgebra_glm::vec3(local[(0, 2)], local[(1, 2)], local[(2, 2)]);
731    let scale = nalgebra_glm::vec3(
732        basis_x.magnitude(),
733        basis_y.magnitude(),
734        basis_z.magnitude(),
735    );
736    let rotation_matrix = nalgebra_glm::Mat3::from_columns(&[
737        basis_x / scale.x.max(f32::EPSILON),
738        basis_y / scale.y.max(f32::EPSILON),
739        basis_z / scale.z.max(f32::EPSILON),
740    ]);
741    let rotation = nalgebra_glm::mat3_to_quat(&rotation_matrix);
742
743    if parent.is_some() {
744        world.ecs.worlds[CORE].add_components(child, PARENT);
745    }
746    update_parent(world, child, parent);
747    world.set(
748        child,
749        LocalTransform {
750            translation,
751            rotation,
752            scale,
753        },
754    );
755}
756
757#[cfg(feature = "picking")]
758pub(crate) fn is_reserved(world: &World, entity: Entity) -> bool {
759    world
760        .get::<nightshade::ecs::primitives::Name>(entity)
761        .is_some_and(|name| name.0.starts_with(crate::runner::RESERVED_PREFIX))
762}
763
764pub(crate) fn api_material_name(entity: Entity) -> String {
765    format!("{MATERIAL_PREFIX}{}", entity.id)
766}
767
768fn mesh_name(shape: Shape) -> &'static str {
769    match shape {
770        Shape::Cube => "Cube",
771        Shape::Sphere => "Sphere",
772        Shape::Cylinder => "Cylinder",
773        Shape::Cone => "Cone",
774        Shape::Torus => "Torus",
775        Shape::Plane => "Plane",
776    }
777}
778
779#[cfg(feature = "physics")]
780fn dynamic_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
781    match shape {
782        Shape::Cube => ColliderComponent::new_cuboid(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5),
783        Shape::Sphere => ColliderComponent::new_ball(scale.x),
784        Shape::Cylinder => ColliderComponent::new_cylinder(scale.y * 0.5, scale.x * 0.5),
785        Shape::Cone => ColliderComponent::new_cone(scale.y * 0.5, scale.x * 0.5),
786        Shape::Torus => ColliderComponent {
787            shape: ColliderShape::ConvexMesh {
788                vertices: scaled_mesh_points(world, "Torus", scale),
789            },
790            ..Default::default()
791        },
792        Shape::Plane => ColliderComponent::new_cuboid(scale.x, 0.05, scale.z),
793    }
794}
795
796#[cfg(feature = "physics")]
797fn static_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
798    match shape {
799        Shape::Torus | Shape::Plane => {
800            let shape_mesh_name = mesh_name(shape);
801            ColliderComponent {
802                shape: ColliderShape::TriMesh {
803                    vertices: scaled_mesh_points(world, shape_mesh_name, scale),
804                    indices: mesh_triangles(world, shape_mesh_name),
805                },
806                ..Default::default()
807            }
808        }
809        _ => dynamic_collider(world, shape, scale),
810    }
811}
812
813#[cfg(feature = "physics")]
814fn scaled_mesh_points(world: &World, mesh_name: &str, scale: Vec3) -> Vec<[f32; 3]> {
815    registry_entry_by_name(
816        &world
817            .res::<nightshade::ecs::asset_state::AssetState>()
818            .mesh_cache
819            .registry,
820        mesh_name,
821    )
822    .map(|mesh| {
823        mesh.vertices
824            .iter()
825            .map(|vertex| {
826                [
827                    vertex.position[0] * scale.x,
828                    vertex.position[1] * scale.y,
829                    vertex.position[2] * scale.z,
830                ]
831            })
832            .collect()
833    })
834    .unwrap_or_default()
835}
836
837#[cfg(feature = "physics")]
838fn mesh_triangles(world: &World, mesh_name: &str) -> Vec<[u32; 3]> {
839    registry_entry_by_name(
840        &world
841            .res::<nightshade::ecs::asset_state::AssetState>()
842            .mesh_cache
843            .registry,
844        mesh_name,
845    )
846    .map(|mesh| {
847        mesh.indices
848            .chunks_exact(3)
849            .map(|triangle| [triangle[0], triangle[1], triangle[2]])
850            .collect()
851    })
852    .unwrap_or_default()
853}
854
855#[cfg(feature = "physics")]
856fn attach_body(
857    world: &mut World,
858    entity: Entity,
859    body: RigidBodyComponent,
860    collider: ColliderComponent,
861    dynamic: bool,
862) {
863    let mut flags = RIGID_BODY | COLLIDER;
864    if dynamic {
865        flags |= COLLISION_LISTENER | PHYSICS_INTERPOLATION;
866    }
867    world.ecs.worlds[CORE].add_components(entity, flags);
868    world.set(entity, body);
869    world.set(entity, collider);
870    if dynamic {
871        reset_physics_interpolation(world, entity);
872        if let Some(interpolation) =
873            world.get_mut::<nightshade::plugins::physics::components::PhysicsInterpolation>(entity)
874        {
875            interpolation.enabled = true;
876        }
877    }
878}