nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::lattice::components::Lattice;
use crate::ecs::lines::components::Line;
use crate::ecs::world::{
    Entity, GLOBAL_TRANSFORM, LINES, LOCAL_TRANSFORM, LOCAL_TRANSFORM_DIRTY, VISIBILITY, World,
};
use nalgebra_glm::{Vec3, Vec4, vec3};
use std::f32::consts::PI;

const BASE_COLOR: Vec4 = Vec4::new(0.4, 0.4, 0.4, 0.5);
const POINT_COLOR: Vec4 = Vec4::new(0.0, 0.8, 1.0, 1.0);
const GRID_COLOR: Vec4 = Vec4::new(0.0, 0.6, 0.8, 0.8);
const SELECTED_COLOR: Vec4 = Vec4::new(1.0, 1.0, 0.0, 1.0);

pub struct LatticeDebugState {
    pub debug_entity: Option<Entity>,
    pub enabled: bool,
    pub selected_point: Option<(usize, usize, usize)>,
    pub last_version: u32,
}

impl Default for LatticeDebugState {
    fn default() -> Self {
        Self {
            debug_entity: None,
            enabled: true,
            selected_point: None,
            last_version: u32::MAX,
        }
    }
}

pub fn lattice_debug_draw(
    world: &mut World,
    lattice_entity: Entity,
    state: &mut LatticeDebugState,
) {
    if !state.enabled {
        if let Some(entity) = state.debug_entity
            && let Some(visibility) = world.core.get_visibility_mut(entity)
        {
            visibility.visible = false;
        }
        return;
    }

    let Some(lattice) = world.core.get_lattice(lattice_entity).cloned() else {
        return;
    };

    let debug_entity = match state.debug_entity {
        Some(entity) => entity,
        None => {
            let entity = world.spawn_entities(
                LINES | VISIBILITY | LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY,
                1,
            )[0];
            state.debug_entity = Some(entity);
            entity
        }
    };

    if let Some(visibility) = world.core.get_visibility_mut(debug_entity) {
        visibility.visible = true;
    }

    if state.last_version == lattice.version && state.selected_point.is_none() {
        return;
    }

    let mut lines = Vec::new();

    draw_lattice_points(&lattice, &mut lines, state.selected_point);
    draw_lattice_grid(&lattice, &mut lines);

    if let Some(lines_component) = world.core.get_lines_mut(debug_entity) {
        lines_component.lines = lines;
        lines_component.mark_dirty();
    }

    state.last_version = lattice.version;
}

fn draw_lattice_points(
    lattice: &Lattice,
    lines: &mut Vec<Line>,
    selected: Option<(usize, usize, usize)>,
) {
    let [nx, ny, nz] = lattice.dimensions;
    let sphere_radius = 0.08;
    let segments = 12;

    for z in 0..nz {
        for y in 0..ny {
            for x in 0..nx {
                let point = lattice.get_point(x, y, z);
                let is_selected = selected == Some((x, y, z));
                let color = if is_selected {
                    SELECTED_COLOR
                } else {
                    POINT_COLOR
                };

                draw_sphere_wireframe(lines, point, sphere_radius, segments, color);

                let base_point = lattice.base_points[lattice.get_index(x, y, z)];
                if (point - base_point).norm() > 0.001 {
                    draw_sphere_wireframe(lines, base_point, sphere_radius * 0.5, 8, BASE_COLOR);
                }
            }
        }
    }
}

fn draw_lattice_grid(lattice: &Lattice, lines: &mut Vec<Line>) {
    let [nx, ny, nz] = lattice.dimensions;

    for z in 0..nz {
        for y in 0..ny {
            for x in 0..nx {
                let point = lattice.get_point(x, y, z);

                if x + 1 < nx {
                    let neighbor = lattice.get_point(x + 1, y, z);
                    lines.push(Line {
                        start: point,
                        end: neighbor,
                        color: GRID_COLOR,
                    });
                }

                if y + 1 < ny {
                    let neighbor = lattice.get_point(x, y + 1, z);
                    lines.push(Line {
                        start: point,
                        end: neighbor,
                        color: GRID_COLOR,
                    });
                }

                if z + 1 < nz {
                    let neighbor = lattice.get_point(x, y, z + 1);
                    lines.push(Line {
                        start: point,
                        end: neighbor,
                        color: GRID_COLOR,
                    });
                }
            }
        }
    }
}

fn draw_sphere_wireframe(
    lines: &mut Vec<Line>,
    center: Vec3,
    radius: f32,
    segments: usize,
    color: Vec4,
) {
    draw_circle(
        lines,
        center,
        radius,
        segments,
        vec3(1.0, 0.0, 0.0),
        vec3(0.0, 1.0, 0.0),
        color,
    );
    draw_circle(
        lines,
        center,
        radius,
        segments,
        vec3(1.0, 0.0, 0.0),
        vec3(0.0, 0.0, 1.0),
        color,
    );
    draw_circle(
        lines,
        center,
        radius,
        segments,
        vec3(0.0, 1.0, 0.0),
        vec3(0.0, 0.0, 1.0),
        color,
    );
}

fn draw_circle(
    lines: &mut Vec<Line>,
    center: Vec3,
    radius: f32,
    segments: usize,
    axis_a: Vec3,
    axis_b: Vec3,
    color: Vec4,
) {
    for index in 0..segments {
        let angle1 = (index as f32 / segments as f32) * 2.0 * PI;
        let angle2 = ((index + 1) as f32 / segments as f32) * 2.0 * PI;

        let p1 = center + axis_a * (angle1.cos() * radius) + axis_b * (angle1.sin() * radius);
        let p2 = center + axis_a * (angle2.cos() * radius) + axis_b * (angle2.sin() * radius);

        lines.push(Line {
            start: p1,
            end: p2,
            color,
        });
    }
}

pub fn get_lattice_point_at_screen_position(
    world: &World,
    lattice_entity: Entity,
    screen_pos: nalgebra_glm::Vec2,
    screen_size: nalgebra_glm::Vec2,
) -> Option<(usize, usize, usize)> {
    let lattice = world.core.get_lattice(lattice_entity)?;
    let camera_entity = world.resources.active_camera?;
    let camera = world.core.get_camera(camera_entity)?;
    let camera_transform = world.core.get_global_transform(camera_entity)?;

    let view_matrix = nalgebra_glm::inverse(&camera_transform.0);
    let projection_matrix = camera
        .projection
        .matrix_with_aspect(screen_size.x / screen_size.y);
    let view_projection = projection_matrix * view_matrix;

    let [nx, ny, nz] = lattice.dimensions;
    let mut closest_point: Option<(usize, usize, usize)> = None;
    let mut closest_distance = f32::MAX;
    let pick_radius = 20.0;

    for z in 0..nz {
        for y in 0..ny {
            for x in 0..nx {
                let world_pos = lattice.get_point(x, y, z);
                let clip = view_projection
                    * nalgebra_glm::vec4(world_pos.x, world_pos.y, world_pos.z, 1.0);

                if clip.w <= 0.0 {
                    continue;
                }

                let ndc = nalgebra_glm::vec3(clip.x / clip.w, clip.y / clip.w, clip.z / clip.w);
                let screen_x = (ndc.x + 1.0) * 0.5 * screen_size.x;
                let screen_y = (1.0 - ndc.y) * 0.5 * screen_size.y;

                let dx = screen_x - screen_pos.x;
                let dy = screen_y - screen_pos.y;
                let dist = (dx * dx + dy * dy).sqrt();

                if dist < pick_radius && dist < closest_distance {
                    closest_distance = dist;
                    closest_point = Some((x, y, z));
                }
            }
        }
    }

    closest_point
}