1use crate::palette::WHITE;
5use crate::runner::MATERIAL_PREFIX;
6use nightshade::prelude::nalgebra_glm::Mat4;
7use nightshade::prelude::*;
8
9pub use nightshade::prelude::despawn_recursive_immediate as despawn;
10pub use nightshade::prelude::spawn_cone_at as spawn_cone;
11pub use nightshade::prelude::spawn_cube_at as spawn_cube;
12pub use nightshade::prelude::spawn_cylinder_at as spawn_cylinder;
13pub use nightshade::prelude::spawn_plane_at as spawn_plane;
14pub use nightshade::prelude::spawn_sphere_at as spawn_sphere;
15pub use nightshade::prelude::spawn_torus_at as spawn_torus;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum Shape {
20 #[default]
21 Cube,
22 Sphere,
23 Cylinder,
24 Cone,
25 Torus,
26 Plane,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Default)]
36pub enum Body {
37 #[default]
38 None,
39 Static,
40 Dynamic {
41 mass: f32,
42 },
43}
44
45pub struct Object {
57 pub shape: Shape,
58 pub position: Vec3,
59 pub scale: Vec3,
60 pub color: [f32; 4],
61 pub body: Body,
62}
63
64impl Default for Object {
65 fn default() -> Self {
66 Self {
67 shape: Shape::Cube,
68 position: Vec3::zeros(),
69 scale: Vec3::new(1.0, 1.0, 1.0),
70 color: WHITE,
71 body: Body::None,
72 }
73 }
74}
75
76pub fn spawn_object(world: &mut World, object: Object) -> Entity {
78 let entity = spawn_mesh_at(
79 world,
80 mesh_name(object.shape),
81 object.position,
82 object.scale,
83 );
84 crate::appearance::set_color(world, entity, object.color);
85 match object.body {
86 Body::None => {}
87 #[cfg(feature = "physics")]
88 Body::Static => {
89 let collider = static_collider(world, object.shape, object.scale)
90 .with_friction(0.8)
91 .with_restitution(0.1);
92 attach_body(
93 world,
94 entity,
95 RigidBodyComponent::new_static().with_translation(
96 object.position.x,
97 object.position.y,
98 object.position.z,
99 ),
100 collider,
101 false,
102 );
103 }
104 #[cfg(feature = "physics")]
105 Body::Dynamic { mass } => {
106 let collider = dynamic_collider(world, object.shape, object.scale)
107 .with_friction(0.7)
108 .with_restitution(0.2);
109 attach_body(
110 world,
111 entity,
112 RigidBodyComponent::new_dynamic()
113 .with_translation(object.position.x, object.position.y, object.position.z)
114 .with_mass(mass),
115 collider,
116 true,
117 );
118 }
119 #[cfg(not(feature = "physics"))]
120 Body::Static | Body::Dynamic { .. } => {}
121 }
122 entity
123}
124
125pub fn spawn_objects(world: &mut World, object: Object, positions: &[Vec3]) -> Vec<Entity> {
129 let mut entities = Vec::with_capacity(positions.len());
130 let mut shared_material: Option<String> = None;
131 #[cfg(feature = "physics")]
132 let mut collider_template: Option<ColliderComponent> = None;
133
134 for &position in positions {
135 let entity = spawn_mesh_at(world, mesh_name(object.shape), position, object.scale);
136 match shared_material.as_deref() {
137 None => {
138 crate::appearance::set_color(world, entity, object.color);
139 shared_material = world
140 .core
141 .get_material_ref(entity)
142 .map(|material_ref| material_ref.name.clone());
143 }
144 Some(name) => {
145 let name = name.to_string();
146 adopt_shared_material(world, entity, &name);
147 }
148 }
149
150 #[cfg(feature = "physics")]
151 match object.body {
152 Body::None => {}
153 Body::Static => {
154 let collider = collider_template
155 .get_or_insert_with(|| {
156 static_collider(world, object.shape, object.scale)
157 .with_friction(0.8)
158 .with_restitution(0.1)
159 })
160 .clone();
161 attach_body(
162 world,
163 entity,
164 RigidBodyComponent::new_static()
165 .with_translation(position.x, position.y, position.z),
166 collider,
167 false,
168 );
169 }
170 Body::Dynamic { mass } => {
171 let collider = collider_template
172 .get_or_insert_with(|| {
173 dynamic_collider(world, object.shape, object.scale)
174 .with_friction(0.7)
175 .with_restitution(0.2)
176 })
177 .clone();
178 attach_body(
179 world,
180 entity,
181 RigidBodyComponent::new_dynamic()
182 .with_translation(position.x, position.y, position.z)
183 .with_mass(mass),
184 collider,
185 true,
186 );
187 }
188 }
189
190 entities.push(entity);
191 }
192 entities
193}
194
195pub fn spawn_instanced(
200 world: &mut World,
201 shape: Shape,
202 transforms: Vec<InstanceTransform>,
203 color: [f32; 4],
204) -> Entity {
205 let shape_mesh_name = mesh_name(shape);
206 ensure_primitive_mesh(world, shape_mesh_name);
207 let fallback = format!(
208 "api::material::instanced::{:.4}_{:.4}_{:.4}_{:.4}",
209 color[0], color[1], color[2], color[3]
210 );
211 let material_name = nightshade::ecs::material::resources::material_registry_find_or_insert(
212 &mut world.resources.assets.material_registry,
213 fallback,
214 Material {
215 base_color: color,
216 ..Default::default()
217 },
218 );
219 spawn_instanced_mesh_with_material(world, shape_mesh_name, transforms, &material_name)
220}
221
222fn adopt_shared_material(world: &mut World, entity: Entity, name: &str) {
223 let previous = world
224 .core
225 .get_material_ref(entity)
226 .map(|material_ref| material_ref.name.clone());
227 if let Some(previous_name) = previous
228 && let Some((index, _)) = registry_lookup_index(
229 &world.resources.assets.material_registry.registry,
230 &previous_name,
231 )
232 {
233 registry_remove_reference(
234 &mut world.resources.assets.material_registry.registry,
235 index,
236 );
237 }
238 if let Some((index, _)) =
239 registry_lookup_index(&world.resources.assets.material_registry.registry, name)
240 {
241 registry_add_reference(
242 &mut world.resources.assets.material_registry.registry,
243 index,
244 );
245 }
246 world
247 .core
248 .set_material_ref(entity, MaterialRef::new(name.to_string()));
249 world.resources.mesh_render_state.mark_entity_added(entity);
250}
251
252pub(crate) fn ensure_primitive_mesh(world: &mut World, mesh_name: &str) {
253 use nightshade::ecs::mesh::components::{
254 create_cone_mesh, create_cube_mesh, create_cylinder_mesh, create_plane_mesh,
255 create_sphere_mesh, create_torus_mesh,
256 };
257 if !world
258 .resources
259 .assets
260 .mesh_cache
261 .registry
262 .name_to_index
263 .contains_key(mesh_name)
264 {
265 let mesh = match mesh_name {
266 "Cube" => create_cube_mesh(),
267 "Sphere" => create_sphere_mesh(1.0, 16),
268 "Plane" => create_plane_mesh(2.0),
269 "Torus" => create_torus_mesh(1.0, 0.3, 32, 16),
270 "Cylinder" => create_cylinder_mesh(0.5, 1.0, 16),
271 _ => create_cone_mesh(0.5, 1.0, 16),
272 };
273 mesh_cache_insert(
274 &mut world.resources.assets.mesh_cache,
275 mesh_name.to_string(),
276 mesh,
277 );
278 }
279 if let Some((index, _)) =
280 registry_lookup_index(&world.resources.assets.mesh_cache.registry, mesh_name)
281 {
282 registry_add_reference(&mut world.resources.assets.mesh_cache.registry, index);
283 }
284}
285
286pub fn spawn_cloth_sheet(world: &mut World, position: Vec3, width: f32, height: f32) -> Entity {
291 spawn_cloth(
292 world,
293 Cloth {
294 width,
295 height,
296 ..Default::default()
297 },
298 position,
299 "Cloth".to_string(),
300 )
301}
302
303pub fn set_visible(world: &mut World, entity: Entity, visible: bool) {
305 if let Some(visibility) = world.core.get_visibility_mut(entity) {
306 visibility.visible = visible;
307 }
308}
309
310pub fn spawn_floor(world: &mut World, half_extent: f32) -> Entity {
314 let entity = spawn_mesh_at(
315 world,
316 "Plane",
317 Vec3::zeros(),
318 Vec3::new(half_extent, 1.0, half_extent),
319 );
320 #[cfg(feature = "physics")]
321 attach_body(
322 world,
323 entity,
324 RigidBodyComponent::new_static().with_translation(0.0, -0.05, 0.0),
325 ColliderComponent::new_cuboid(half_extent, 0.05, half_extent)
326 .with_friction(0.8)
327 .with_restitution(0.1),
328 false,
329 );
330 entity
331}
332
333pub fn spawn_model(world: &mut World, glb_bytes: &[u8], position: Vec3) -> Entity {
336 let mut result =
337 import_gltf_from_bytes(glb_bytes).expect("failed to import the glb model bytes");
338 nightshade::ecs::loading::queue_gltf_load(world, &mut result);
339 let prefab = &result.prefabs[0];
340 nightshade::ecs::prefab::commands::spawn_prefab_with_skins(
341 world,
342 prefab,
343 &result.animations,
344 &result.skins,
345 position,
346 )
347}
348
349pub fn play_animation(world: &mut World, entity: Entity, clip_index: usize) {
351 if let Some(player) = world.core.get_animation_player_mut(entity) {
352 player.play(clip_index);
353 }
354}
355
356pub fn set_animation_looping(world: &mut World, entity: Entity, looping: bool) {
358 if let Some(player) = world.core.get_animation_player_mut(entity) {
359 player.looping = looping;
360 }
361}
362
363pub fn spawn_group(world: &mut World, position: Vec3) -> Entity {
366 let entity = spawn_entities(
367 world,
368 NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM,
369 1,
370 )[0];
371 world.core.set_name(entity, Name("Group".to_string()));
372 assign_local_transform(
373 world,
374 entity,
375 LocalTransform {
376 translation: position,
377 ..Default::default()
378 },
379 );
380 entity
381}
382
383pub fn set_parent(world: &mut World, child: Entity, parent: Option<Entity>) {
386 let child_world = crate::placement::world_matrix(world, child);
387 let parent_world = parent
388 .map(|parent_entity| crate::placement::world_matrix(world, parent_entity))
389 .unwrap_or_else(Mat4::identity);
390 let local = nalgebra_glm::inverse(&parent_world) * child_world;
391
392 let translation = nalgebra_glm::vec3(local[(0, 3)], local[(1, 3)], local[(2, 3)]);
393 let basis_x = nalgebra_glm::vec3(local[(0, 0)], local[(1, 0)], local[(2, 0)]);
394 let basis_y = nalgebra_glm::vec3(local[(0, 1)], local[(1, 1)], local[(2, 1)]);
395 let basis_z = nalgebra_glm::vec3(local[(0, 2)], local[(1, 2)], local[(2, 2)]);
396 let scale = nalgebra_glm::vec3(
397 basis_x.magnitude(),
398 basis_y.magnitude(),
399 basis_z.magnitude(),
400 );
401 let rotation_matrix = nalgebra_glm::Mat3::from_columns(&[
402 basis_x / scale.x.max(f32::EPSILON),
403 basis_y / scale.y.max(f32::EPSILON),
404 basis_z / scale.z.max(f32::EPSILON),
405 ]);
406 let rotation = nalgebra_glm::mat3_to_quat(&rotation_matrix);
407
408 if parent.is_some() {
409 world.core.add_components(child, PARENT);
410 }
411 update_parent(
412 world,
413 child,
414 parent.map(|parent_entity| Parent(Some(parent_entity))),
415 );
416 assign_local_transform(
417 world,
418 child,
419 LocalTransform {
420 translation,
421 rotation,
422 scale,
423 },
424 );
425}
426
427#[cfg(feature = "picking")]
428pub(crate) fn is_reserved(world: &World, entity: Entity) -> bool {
429 world
430 .core
431 .get_name(entity)
432 .is_some_and(|name| name.0.starts_with(crate::runner::RESERVED_PREFIX))
433}
434
435pub(crate) fn api_material_name(entity: Entity) -> String {
436 format!("{MATERIAL_PREFIX}{}", entity.id)
437}
438
439fn mesh_name(shape: Shape) -> &'static str {
440 match shape {
441 Shape::Cube => "Cube",
442 Shape::Sphere => "Sphere",
443 Shape::Cylinder => "Cylinder",
444 Shape::Cone => "Cone",
445 Shape::Torus => "Torus",
446 Shape::Plane => "Plane",
447 }
448}
449
450#[cfg(feature = "physics")]
451fn dynamic_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
452 match shape {
453 Shape::Cube => ColliderComponent::new_cuboid(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5),
454 Shape::Sphere => ColliderComponent::new_ball(scale.x),
455 Shape::Cylinder => ColliderComponent::new_cylinder(scale.y * 0.5, scale.x * 0.5),
456 Shape::Cone => ColliderComponent::new_cone(scale.y * 0.5, scale.x * 0.5),
457 Shape::Torus => ColliderComponent {
458 shape: ColliderShape::ConvexMesh {
459 vertices: scaled_mesh_points(world, "Torus", scale),
460 },
461 ..Default::default()
462 },
463 Shape::Plane => ColliderComponent::new_cuboid(scale.x, 0.05, scale.z),
464 }
465}
466
467#[cfg(feature = "physics")]
468fn static_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
469 match shape {
470 Shape::Torus | Shape::Plane => {
471 let shape_mesh_name = mesh_name(shape);
472 ColliderComponent {
473 shape: ColliderShape::TriMesh {
474 vertices: scaled_mesh_points(world, shape_mesh_name, scale),
475 indices: mesh_triangles(world, shape_mesh_name),
476 },
477 ..Default::default()
478 }
479 }
480 _ => dynamic_collider(world, shape, scale),
481 }
482}
483
484#[cfg(feature = "physics")]
485fn scaled_mesh_points(world: &World, mesh_name: &str, scale: Vec3) -> Vec<[f32; 3]> {
486 registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
487 .map(|mesh| {
488 mesh.vertices
489 .iter()
490 .map(|vertex| {
491 [
492 vertex.position[0] * scale.x,
493 vertex.position[1] * scale.y,
494 vertex.position[2] * scale.z,
495 ]
496 })
497 .collect()
498 })
499 .unwrap_or_default()
500}
501
502#[cfg(feature = "physics")]
503fn mesh_triangles(world: &World, mesh_name: &str) -> Vec<[u32; 3]> {
504 registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
505 .map(|mesh| {
506 mesh.indices
507 .chunks_exact(3)
508 .map(|triangle| [triangle[0], triangle[1], triangle[2]])
509 .collect()
510 })
511 .unwrap_or_default()
512}
513
514#[cfg(feature = "physics")]
515fn attach_body(
516 world: &mut World,
517 entity: Entity,
518 body: RigidBodyComponent,
519 collider: ColliderComponent,
520 dynamic: bool,
521) {
522 let mut flags = RIGID_BODY | COLLIDER;
523 if dynamic {
524 flags |= COLLISION_LISTENER | PHYSICS_INTERPOLATION;
525 }
526 world.core.add_components(entity, flags);
527 world.core.set_rigid_body(entity, body);
528 world.core.set_collider(entity, collider);
529 if dynamic {
530 reset_physics_interpolation(world, entity);
531 if let Some(interpolation) = world.core.get_physics_interpolation_mut(entity) {
532 interpolation.enabled = true;
533 }
534 }
535}