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