nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::generational_registry::registry_entry_by_name;
use crate::ecs::navmesh::{RecastNavMeshConfig, generate_navmesh_recast};
use crate::ecs::physics::{ColliderComponent, ColliderShape, RigidBodyComponent};
use crate::ecs::transform::queries::query_descendants;
use crate::ecs::world::components::Parent;
use crate::ecs::world::{COLLIDER, NAME, PARENT, RIGID_BODY};
use crate::editor::selection::EntitySelection;
use crate::prelude::*;

pub fn navmesh_section_ui(selection: &EntitySelection, world: &mut World, ui: &mut egui::Ui) {
    ui.group(|ui| {
        ui.label(egui::RichText::new("NavMesh").strong());
        ui.separator();

        let navmesh = &world.resources.navmesh;
        let triangle_count = navmesh.triangles.len();
        let vertex_count = navmesh.vertices.len();
        let connection_count: usize = navmesh.adjacency.values().map(|v| v.len()).sum();

        if triangle_count > 0 {
            ui.label(format!("Triangles: {}", triangle_count));
            ui.label(format!("Vertices: {}", vertex_count));
            ui.label(format!("Connections: {}", connection_count));
            ui.separator();
        } else {
            ui.label("No navmesh generated");
            ui.separator();
        }

        if let Some(selected_entity) = selection.primary() {
            if ui.button("Generate NavMesh").clicked() {
                generate_navmesh_from_entity(world, selected_entity);
            }
            ui.label(
                egui::RichText::new("Generates from selected entity and children")
                    .small()
                    .weak(),
            );
        } else {
            ui.add_enabled(false, egui::Button::new("Generate NavMesh"));
            ui.label(
                egui::RichText::new("Select an entity to generate navmesh")
                    .small()
                    .weak(),
            );
        }

        if triangle_count > 0 && ui.button("Clear NavMesh").clicked() {
            world.resources.navmesh.clear();
        }
    });
}

struct TransformedMesh {
    vertices: Vec<[f32; 3]>,
    indices: Vec<[u32; 3]>,
}

fn extract_mesh_with_transform(
    world: &World,
    entity: Entity,
    transform: nalgebra_glm::Mat4,
) -> Option<TransformedMesh> {
    let mesh_name = world.get_render_mesh(entity)?.name.clone();

    let mesh = registry_entry_by_name(&world.resources.mesh_cache.registry, &mesh_name)?;

    let vertices: Vec<[f32; 3]> = mesh
        .vertices
        .iter()
        .map(|vertex| {
            let pos = nalgebra_glm::vec4(
                vertex.position[0],
                vertex.position[1],
                vertex.position[2],
                1.0,
            );
            let transformed = transform * pos;
            [transformed.x, transformed.y, transformed.z]
        })
        .collect();

    let indices: Vec<[u32; 3]> = mesh
        .indices
        .chunks(3)
        .filter(|chunk| chunk.len() == 3)
        .map(|chunk| [chunk[0], chunk[1], chunk[2]])
        .collect();

    Some(TransformedMesh { vertices, indices })
}

fn extract_transformed_mesh(world: &World, entity: Entity) -> Option<TransformedMesh> {
    let global_transform = world
        .get_global_transform(entity)
        .map(|t| t.0)
        .unwrap_or_else(nalgebra_glm::Mat4::identity);
    extract_mesh_with_transform(world, entity, global_transform)
}

fn extract_local_mesh(world: &World, entity: Entity) -> Option<TransformedMesh> {
    extract_mesh_with_transform(world, entity, nalgebra_glm::Mat4::identity())
}

fn collect_entity_and_descendants(world: &World, entity: Entity) -> Vec<Entity> {
    let mut entities = vec![entity];
    entities.extend(query_descendants(world, entity));
    entities
}

fn generate_navmesh_from_entity(world: &mut World, entity: Entity) {
    let mut all_vertices: Vec<[f32; 3]> = Vec::new();
    let mut all_indices: Vec<[u32; 3]> = Vec::new();

    let entities_to_process = collect_entity_and_descendants(world, entity);

    for entity in entities_to_process {
        if let Some(mesh) = extract_transformed_mesh(world, entity) {
            let base_index = all_vertices.len() as u32;
            all_vertices.extend(mesh.vertices);
            all_indices.extend(mesh.indices.into_iter().map(|tri| {
                [
                    base_index + tri[0],
                    base_index + tri[1],
                    base_index + tri[2],
                ]
            }));
        }
    }

    if all_indices.is_empty() {
        return;
    }

    let config = RecastNavMeshConfig::default();

    if let Some(navmesh) = generate_navmesh_recast(&all_vertices, &all_indices, &config) {
        world.resources.navmesh = navmesh;
    }
}

pub fn colliders_section_ui(selection: &EntitySelection, world: &mut World, ui: &mut egui::Ui) {
    ui.group(|ui| {
        ui.label(egui::RichText::new("Colliders").strong());
        ui.separator();

        let collider_count = world.resources.physics.collider_set.len();

        ui.label(format!("Colliders: {}", collider_count));
        ui.separator();

        if let Some(selected_entity) = selection.primary() {
            if ui.button("Generate Colliders").clicked() {
                generate_colliders_from_entity(world, selected_entity);
            }
            ui.label(
                egui::RichText::new(
                    "Generates trimesh colliders from selected entity and children",
                )
                .small()
                .weak(),
            );
        } else {
            ui.add_enabled(false, egui::Button::new("Generate Colliders"));
            ui.label(
                egui::RichText::new("Select an entity to generate colliders")
                    .small()
                    .weak(),
            );
        }
    });
}

fn generate_colliders_from_entity(world: &mut World, entity: Entity) {
    let entities_to_process = collect_entity_and_descendants(world, entity);

    let mut collider_count = 0;

    for entity in entities_to_process {
        let Some(mesh) = extract_local_mesh(world, entity) else {
            continue;
        };

        if mesh.indices.is_empty() {
            continue;
        }

        let collision_entity = world.spawn_entities(
            NAME | LOCAL_TRANSFORM
                | GLOBAL_TRANSFORM
                | LOCAL_TRANSFORM_DIRTY
                | PARENT
                | RIGID_BODY
                | COLLIDER,
            1,
        )[0];

        world.set_parent(collision_entity, Parent(Some(entity)));

        if let Some(name) = world.get_name_mut(collision_entity) {
            name.0 = format!("Mesh Collision {}", collider_count);
        }

        if let Some(rigid_body) = world.get_rigid_body_mut(collision_entity) {
            *rigid_body = RigidBodyComponent::new_static();
        }

        if let Some(collider) = world.get_collider_mut(collision_entity) {
            *collider = ColliderComponent {
                shape: ColliderShape::TriMesh {
                    vertices: mesh.vertices,
                    indices: mesh.indices,
                },
                friction: 0.7,
                restitution: 0.0,
                ..Default::default()
            };
        }

        collider_count += 1;
    }
}