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.resources.assets.material_registry,
283        fallback,
284        Material {
285            base_color: color,
286            ..Default::default()
287        },
288    );
289    spawn_instanced_mesh_with_material(world, shape_mesh_name, transforms, &material_name)
290}
291
292/// Spawns an instanced mesh of `shape` that draws every transform in one batch
293/// using the registry material named `material`, for large crowds sharing a
294/// custom look. Register the material first with
295/// [`register_material`](crate::prelude::register_material). Returns the batch
296/// entity; restream its transforms with [`set_instances`].
297pub fn spawn_instanced_with_material(
298    world: &mut World,
299    shape: Shape,
300    transforms: Vec<InstanceTransform>,
301    material: &str,
302) -> Entity {
303    let shape_mesh_name = mesh_name(shape);
304    ensure_primitive_mesh(world, shape_mesh_name);
305    spawn_instanced_mesh_with_material(world, shape_mesh_name, transforms, material)
306}
307
308/// Replaces an instanced batch's transforms and flags the renderer to reupload
309/// its instance buffer, for streaming worlds and animated instance sets that
310/// change every frame.
311pub fn set_instances(world: &mut World, batch: Entity, transforms: Vec<InstanceTransform>) {
312    if let Some(instanced) =
313        world.get_mut::<nightshade::ecs::mesh::components::InstancedMesh>(batch)
314    {
315        instanced.set_instances(transforms);
316    }
317    world
318        .resources
319        .mesh_render_state
320        .mark_instanced_meshes_changed();
321}
322
323fn adopt_shared_material(world: &mut World, entity: Entity, name: &str) {
324    let previous = world
325        .get::<nightshade::ecs::material::components::MaterialRef>(entity)
326        .map(|material_ref| material_ref.name.clone());
327    if let Some(previous_name) = previous
328        && let Some((index, _)) = registry_lookup_index(
329            &world.resources.assets.material_registry.registry,
330            &previous_name,
331        )
332    {
333        registry_remove_reference(
334            &mut world.resources.assets.material_registry.registry,
335            index,
336        );
337    }
338    if let Some((index, _)) =
339        registry_lookup_index(&world.resources.assets.material_registry.registry, name)
340    {
341        registry_add_reference(
342            &mut world.resources.assets.material_registry.registry,
343            index,
344        );
345    }
346    world.set(entity, MaterialRef::new(name.to_string()));
347    world
348        .resources
349        .mesh_render_state
350        .mark_entity_added(render_entity(entity));
351}
352
353pub(crate) fn ensure_primitive_mesh(world: &mut World, mesh_name: &str) {
354    use nightshade::render::procedural_meshes::{
355        create_cone_mesh, create_cube_mesh, create_cylinder_mesh, create_plane_mesh,
356        create_sphere_mesh, create_torus_mesh,
357    };
358    if !world
359        .resources
360        .assets
361        .mesh_cache
362        .registry
363        .name_to_index
364        .contains_key(mesh_name)
365    {
366        let mesh = match mesh_name {
367            "Cube" => create_cube_mesh(),
368            "Sphere" => create_sphere_mesh(1.0, 16),
369            "Plane" => create_plane_mesh(2.0),
370            "Torus" => create_torus_mesh(1.0, 0.3, 32, 16),
371            "Cylinder" => create_cylinder_mesh(0.5, 1.0, 16),
372            _ => create_cone_mesh(0.5, 1.0, 16),
373        };
374        mesh_cache_insert(
375            &mut world.resources.assets.mesh_cache,
376            mesh_name.to_string(),
377            mesh,
378        );
379    }
380    if let Some((index, _)) =
381        registry_lookup_index(&world.resources.assets.mesh_cache.registry, mesh_name)
382    {
383        registry_add_reference(&mut world.resources.assets.mesh_cache.registry, index);
384    }
385}
386
387/// Spawns a simulated cloth sheet hanging from `position`, pinned along its
388/// top row. It drapes, collides with the ground, and responds to
389/// `world.resources.wind`. Color it like anything else with
390/// [`set_color`](crate::prelude::set_color).
391pub fn spawn_cloth_sheet(world: &mut World, position: Vec3, width: f32, height: f32) -> Entity {
392    spawn_cloth(
393        world,
394        Cloth {
395            width,
396            height,
397            ..Default::default()
398        },
399        position,
400        "Cloth".to_string(),
401    )
402}
403
404/// Shows or hides the entity without despawning it.
405pub fn set_visible(world: &mut World, entity: Entity, visible: bool) {
406    if let Some(visibility) = world.get_mut::<nightshade::ecs::primitives::Visibility>(entity) {
407        visibility.visible = visible;
408    }
409}
410
411/// Spawns a flat ground plane reaching `half_extent` in each direction. With
412/// the `physics` feature it carries a static collider so dynamic objects land
413/// on it.
414pub fn spawn_floor(world: &mut World, half_extent: f32) -> Entity {
415    let entity = spawn_mesh_at(
416        world,
417        "Plane",
418        Vec3::zeros(),
419        Vec3::new(half_extent, 1.0, half_extent),
420    );
421    #[cfg(feature = "physics")]
422    attach_body(
423        world,
424        entity,
425        RigidBodyComponent::new_static().with_translation(0.0, -0.05, 0.0),
426        ColliderComponent::new_cuboid(half_extent, 0.05, half_extent)
427            .with_friction(0.8)
428            .with_restitution(0.1),
429        false,
430    );
431    entity
432}
433
434/// Spawns a glb model with its textures, materials, skins, and animations.
435/// Panics with the import error when the bytes are not a valid glb.
436pub fn spawn_model(world: &mut World, glb_bytes: &[u8], position: Vec3) -> Entity {
437    let mut result =
438        import_gltf_from_bytes(glb_bytes).expect("failed to import the glb model bytes");
439    nightshade::ecs::loading::queue_gltf_load(world, &mut result);
440    let prefab = &result.prefabs[0];
441    nightshade::ecs::prefab::commands::spawn_prefab_with_skins(
442        world,
443        prefab,
444        &result.animations,
445        &result.skins,
446        position,
447    )
448}
449
450/// Starts playing the model's animation clip at `clip_index`.
451pub fn play_animation(world: &mut World, entity: Entity, clip_index: usize) {
452    if let Some(player) =
453        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
454    {
455        player.play(clip_index);
456    }
457}
458
459/// Sets whether the entity's current animation repeats.
460pub fn set_animation_looping(world: &mut World, entity: Entity, looping: bool) {
461    if let Some(player) =
462        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
463    {
464        player.looping = looping;
465    }
466}
467
468/// Plays the animation clip named `clip_name`. Returns whether a clip with that
469/// name exists on the model.
470pub fn play_animation_named(world: &mut World, entity: Entity, clip_name: &str) -> bool {
471    if let Some(player) =
472        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
473        && let Some(index) = player.clips.iter().position(|clip| clip.name == clip_name)
474    {
475        player.play(index);
476        return true;
477    }
478    false
479}
480
481/// Sets the playback speed of the entity's animation. 1.0 is normal, 2.0 doubles
482/// it, a negative value plays backward.
483pub fn set_animation_speed(world: &mut World, entity: Entity, speed: f32) {
484    if let Some(player) =
485        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
486    {
487        player.speed = speed;
488    }
489}
490
491/// Cross-fades from the current clip to `clip_index` over `seconds`, a smooth
492/// transition rather than a hard cut.
493pub fn blend_to_animation(world: &mut World, entity: Entity, clip_index: usize, seconds: f32) {
494    if let Some(player) =
495        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
496    {
497        player.blend_to(clip_index, seconds);
498    }
499}
500
501/// Cross-fades to the clip named `clip_name` over `seconds`. Returns whether a
502/// clip with that name exists.
503pub fn blend_to_animation_named(
504    world: &mut World,
505    entity: Entity,
506    clip_name: &str,
507    seconds: f32,
508) -> bool {
509    if let Some(player) =
510        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
511        && let Some(index) = player.clips.iter().position(|clip| clip.name == clip_name)
512    {
513        player.blend_to(index, seconds);
514        return true;
515    }
516    false
517}
518
519/// Pauses the entity's animation at the current frame.
520pub fn pause_animation(world: &mut World, entity: Entity) {
521    if let Some(player) =
522        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
523    {
524        player.pause();
525    }
526}
527
528/// Resumes a paused animation from where it left off.
529pub fn resume_animation(world: &mut World, entity: Entity) {
530    if let Some(player) =
531        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
532    {
533        player.resume();
534    }
535}
536
537/// Stops the entity's animation and resets it to the first frame.
538pub fn stop_animation(world: &mut World, entity: Entity) {
539    if let Some(player) =
540        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
541    {
542        player.stop();
543    }
544}
545
546/// The names of the entity's animation clips, in clip-index order.
547pub fn animation_clips(world: &World, entity: Entity) -> Vec<String> {
548    world
549        .get::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
550        .map(|player| player.clips.iter().map(|clip| clip.name.clone()).collect())
551        .unwrap_or_default()
552}
553
554/// Adds a named marker at `time` seconds on the clip at `clip_index`. While that
555/// clip plays, crossing the marker publishes an `Event::AnimationEvent` carrying
556/// `name`, the cue to spawn a footstep, land a hit, or trigger an effect. Read
557/// it with [`drain_events`](crate::prelude::drain_events). Returns false if the
558/// entity has no animation player or the clip index is out of range.
559pub fn add_animation_event(
560    world: &mut World,
561    entity: Entity,
562    clip_index: usize,
563    time: f32,
564    name: &str,
565) -> bool {
566    if let Some(player) =
567        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
568        && let Some(clip) = player.clips.get_mut(clip_index)
569    {
570        clip.events
571            .push(nightshade::ecs::animation::components::AnimationEvent {
572                time,
573                name: name.to_string(),
574            });
575        return true;
576    }
577    false
578}
579
580/// Adds a named marker at `time` seconds on the clip named `clip_name`, the
581/// by-name form of [`add_animation_event`].
582pub fn add_animation_event_named(
583    world: &mut World,
584    entity: Entity,
585    clip_name: &str,
586    time: f32,
587    name: &str,
588) -> bool {
589    if let Some(player) =
590        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
591        && let Some(clip) = player.clips.iter_mut().find(|clip| clip.name == clip_name)
592    {
593        clip.events
594            .push(nightshade::ecs::animation::components::AnimationEvent {
595                time,
596                name: name.to_string(),
597            });
598        return true;
599    }
600    false
601}
602
603/// Plays an extra clip on top of the base animation at `weight` (0.0 to 1.0),
604/// blended over the bones the clip targets. Returns the layer's index for
605/// [`set_animation_layer_weight`], or `None` if the entity has no animation
606/// player. A reload, a hit reaction, anything composited over a walk or idle.
607pub fn add_animation_layer(
608    world: &mut World,
609    entity: Entity,
610    clip_index: usize,
611    weight: f32,
612) -> Option<usize> {
613    let player =
614        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)?;
615    let index = player.layers.len();
616    player.layers.push(
617        nightshade::ecs::animation::components::AnimationLayer::new(clip_index).with_weight(weight),
618    );
619    Some(index)
620}
621
622/// Plays an extra clip limited to the named bones, the per-bone mask form of
623/// [`add_animation_layer`]. Pass the bone names the layer should drive, like the
624/// spine and arms for an upper-body wave over a full-body walk.
625pub fn add_animation_layer_masked(
626    world: &mut World,
627    entity: Entity,
628    clip_index: usize,
629    weight: f32,
630    bones: &[&str],
631) -> Option<usize> {
632    let player =
633        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)?;
634    let index = player.layers.len();
635    let mask = bones.iter().map(|bone| bone.to_string()).collect();
636    player.layers.push(
637        nightshade::ecs::animation::components::AnimationLayer::new(clip_index)
638            .with_weight(weight)
639            .with_mask(mask),
640    );
641    Some(index)
642}
643
644/// Sets the blend weight of a layer added by [`add_animation_layer`], for fading
645/// a composited animation in and out. 0.0 disables it without removing it.
646pub fn set_animation_layer_weight(
647    world: &mut World,
648    entity: Entity,
649    layer_index: usize,
650    weight: f32,
651) {
652    if let Some(player) =
653        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
654        && let Some(layer) = player.layers.get_mut(layer_index)
655    {
656        layer.weight = weight;
657    }
658}
659
660/// Removes every animation layer, leaving only the base animation.
661pub fn clear_animation_layers(world: &mut World, entity: Entity) {
662    if let Some(player) =
663        world.get_mut::<nightshade::ecs::animation::components::AnimationPlayer>(entity)
664    {
665        player.layers.clear();
666    }
667}
668
669/// Registers `entity` under `name` so it can be looked up by name, including
670/// through the `named` map a rhai script reads. Use it to hand specific entities
671/// (a HUD label, a player) to scripts that drive them.
672pub fn name_entity(world: &mut World, name: &str, entity: Entity) {
673    world
674        .resources
675        .entities
676        .names
677        .insert(name.to_string(), entity);
678}
679
680/// Spawns an invisible group at `position` for building hierarchies. Parent
681/// things to it and move, rotate, or animate the group to drive them all.
682pub fn spawn_group(world: &mut World, position: Vec3) -> Entity {
683    let entity = spawn_entities(world, NAME | LOCAL_TRANSFORM | GLOBAL_TRANSFORM, 1)[0];
684    world.set(entity, Name("Group".to_string()));
685    world.set(
686        entity,
687        LocalTransform {
688            translation: position,
689            ..Default::default()
690        },
691    );
692    entity
693}
694
695/// Parents `child` to `parent`, or unparents it with `None`, keeping the
696/// child exactly where it is in world space.
697pub fn set_parent(world: &mut World, child: Entity, parent: Option<Entity>) {
698    let child_world = crate::placement::world_matrix(world, child);
699    let parent_world = parent
700        .map(|parent_entity| crate::placement::world_matrix(world, parent_entity))
701        .unwrap_or_else(Mat4::identity);
702    let local = nalgebra_glm::inverse(&parent_world) * child_world;
703
704    let translation = nalgebra_glm::vec3(local[(0, 3)], local[(1, 3)], local[(2, 3)]);
705    let basis_x = nalgebra_glm::vec3(local[(0, 0)], local[(1, 0)], local[(2, 0)]);
706    let basis_y = nalgebra_glm::vec3(local[(0, 1)], local[(1, 1)], local[(2, 1)]);
707    let basis_z = nalgebra_glm::vec3(local[(0, 2)], local[(1, 2)], local[(2, 2)]);
708    let scale = nalgebra_glm::vec3(
709        basis_x.magnitude(),
710        basis_y.magnitude(),
711        basis_z.magnitude(),
712    );
713    let rotation_matrix = nalgebra_glm::Mat3::from_columns(&[
714        basis_x / scale.x.max(f32::EPSILON),
715        basis_y / scale.y.max(f32::EPSILON),
716        basis_z / scale.z.max(f32::EPSILON),
717    ]);
718    let rotation = nalgebra_glm::mat3_to_quat(&rotation_matrix);
719
720    if parent.is_some() {
721        world.ecs.worlds[CORE].add_components(child, PARENT);
722    }
723    update_parent(world, child, parent);
724    world.set(
725        child,
726        LocalTransform {
727            translation,
728            rotation,
729            scale,
730        },
731    );
732}
733
734#[cfg(feature = "picking")]
735pub(crate) fn is_reserved(world: &World, entity: Entity) -> bool {
736    world
737        .get::<nightshade::ecs::primitives::Name>(entity)
738        .is_some_and(|name| name.0.starts_with(crate::runner::RESERVED_PREFIX))
739}
740
741pub(crate) fn api_material_name(entity: Entity) -> String {
742    format!("{MATERIAL_PREFIX}{}", entity.id)
743}
744
745fn mesh_name(shape: Shape) -> &'static str {
746    match shape {
747        Shape::Cube => "Cube",
748        Shape::Sphere => "Sphere",
749        Shape::Cylinder => "Cylinder",
750        Shape::Cone => "Cone",
751        Shape::Torus => "Torus",
752        Shape::Plane => "Plane",
753    }
754}
755
756#[cfg(feature = "physics")]
757fn dynamic_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
758    match shape {
759        Shape::Cube => ColliderComponent::new_cuboid(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5),
760        Shape::Sphere => ColliderComponent::new_ball(scale.x),
761        Shape::Cylinder => ColliderComponent::new_cylinder(scale.y * 0.5, scale.x * 0.5),
762        Shape::Cone => ColliderComponent::new_cone(scale.y * 0.5, scale.x * 0.5),
763        Shape::Torus => ColliderComponent {
764            shape: ColliderShape::ConvexMesh {
765                vertices: scaled_mesh_points(world, "Torus", scale),
766            },
767            ..Default::default()
768        },
769        Shape::Plane => ColliderComponent::new_cuboid(scale.x, 0.05, scale.z),
770    }
771}
772
773#[cfg(feature = "physics")]
774fn static_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
775    match shape {
776        Shape::Torus | Shape::Plane => {
777            let shape_mesh_name = mesh_name(shape);
778            ColliderComponent {
779                shape: ColliderShape::TriMesh {
780                    vertices: scaled_mesh_points(world, shape_mesh_name, scale),
781                    indices: mesh_triangles(world, shape_mesh_name),
782                },
783                ..Default::default()
784            }
785        }
786        _ => dynamic_collider(world, shape, scale),
787    }
788}
789
790#[cfg(feature = "physics")]
791fn scaled_mesh_points(world: &World, mesh_name: &str, scale: Vec3) -> Vec<[f32; 3]> {
792    registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
793        .map(|mesh| {
794            mesh.vertices
795                .iter()
796                .map(|vertex| {
797                    [
798                        vertex.position[0] * scale.x,
799                        vertex.position[1] * scale.y,
800                        vertex.position[2] * scale.z,
801                    ]
802                })
803                .collect()
804        })
805        .unwrap_or_default()
806}
807
808#[cfg(feature = "physics")]
809fn mesh_triangles(world: &World, mesh_name: &str) -> Vec<[u32; 3]> {
810    registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
811        .map(|mesh| {
812            mesh.indices
813                .chunks_exact(3)
814                .map(|triangle| [triangle[0], triangle[1], triangle[2]])
815                .collect()
816        })
817        .unwrap_or_default()
818}
819
820#[cfg(feature = "physics")]
821fn attach_body(
822    world: &mut World,
823    entity: Entity,
824    body: RigidBodyComponent,
825    collider: ColliderComponent,
826    dynamic: bool,
827) {
828    let mut flags = RIGID_BODY | COLLIDER;
829    if dynamic {
830        flags |= COLLISION_LISTENER | PHYSICS_INTERPOLATION;
831    }
832    world.ecs.worlds[CORE].add_components(entity, flags);
833    world.set(entity, body);
834    world.set(entity, collider);
835    if dynamic {
836        reset_physics_interpolation(world, entity);
837        if let Some(interpolation) =
838            world.get_mut::<nightshade::plugins::physics::components::PhysicsInterpolation>(entity)
839        {
840            interpolation.enabled = true;
841        }
842    }
843}