nightshade-editor 0.14.2

Interactive map editor for the Nightshade game engine
use crate::ecs::EditorWorld;
use nightshade::ecs::light::components::LightType;
use nightshade::ecs::lines::components::{Line, Lines};
use nightshade::ecs::world::{GLOBAL_TRANSFORM, LINES, VISIBILITY};
use nightshade::prelude::*;

const SPHERE_SEGMENTS: usize = 32;
const CONE_SEGMENTS: usize = 24;
const ARROW_LENGTH: f32 = 1.5;
const ARROW_HEAD_SIZE: f32 = 0.25;

pub fn update(editor_world: &mut EditorWorld, world: &mut World) {
    let gizmo_entity = ensure_gizmo_entity(editor_world, world);

    let lines = build_lines_for_selection(editor_world, world);

    if lines.is_empty() {
        if let Some(data) = world.core.get_lines(gizmo_entity)
            && data.lines.is_empty()
        {
            return;
        }
        world.core.set_lines(gizmo_entity, Lines::new(Vec::new()));
        return;
    }

    world.core.set_lines(gizmo_entity, Lines::new(lines));
}

fn ensure_gizmo_entity(editor_world: &mut EditorWorld, world: &mut World) -> Entity {
    if let Some(entity) = editor_world.resources.light_gizmos.entity
        && world.core.entity_has_components(entity, LINES)
    {
        return entity;
    }
    let entity = spawn_entities(world, LINES | VISIBILITY | GLOBAL_TRANSFORM, 1)[0];
    world.core.set_lines(entity, Lines::default());
    world
        .core
        .set_visibility(entity, Visibility { visible: true });
    world
        .core
        .set_global_transform(entity, GlobalTransform::default());
    editor_world
        .resources
        .editor_scene
        .register_scaffolding(entity);
    editor_world.resources.light_gizmos.entity = Some(entity);
    entity
}

fn build_lines_for_selection(editor_world: &EditorWorld, world: &World) -> Vec<Line> {
    let Some(selected) = editor_world.resources.ui.selected_entity else {
        return Vec::new();
    };
    let Some(light) = world.core.get_light(selected) else {
        return Vec::new();
    };
    let Some(transform) = world.core.get_global_transform(selected) else {
        return Vec::new();
    };
    let position = transform.translation();
    let forward = transform.forward_vector();
    let right = transform.right_vector();
    let up = transform.up_vector();
    let color = vec4(light.color.x, light.color.y, light.color.z, 1.0);
    match light.light_type {
        LightType::Point => point_light_lines(position, light.range.max(0.05), color),
        LightType::Directional => directional_light_lines(position, forward, right, up, color),
        LightType::Spot => spot_light_lines(
            position,
            forward,
            right,
            up,
            light.outer_cone_angle.max(0.01),
            light.range.max(0.05),
            color,
        ),
    }
}

fn point_light_lines(center: Vec3, radius: f32, color: Vec4) -> Vec<Line> {
    let mut lines = Vec::with_capacity(SPHERE_SEGMENTS * 3);
    let axis_x = Vec3::new(1.0, 0.0, 0.0);
    let axis_y = Vec3::new(0.0, 1.0, 0.0);
    let axis_z = Vec3::new(0.0, 0.0, 1.0);
    push_circle(&mut lines, center, axis_x, axis_y, radius, color);
    push_circle(&mut lines, center, axis_y, axis_z, radius, color);
    push_circle(&mut lines, center, axis_x, axis_z, radius, color);
    lines.push(Line {
        start: center - axis_x * radius,
        end: center + axis_x * radius,
        color,
    });
    lines.push(Line {
        start: center - axis_y * radius,
        end: center + axis_y * radius,
        color,
    });
    lines.push(Line {
        start: center - axis_z * radius,
        end: center + axis_z * radius,
        color,
    });
    lines
}

fn directional_light_lines(
    position: Vec3,
    forward: Vec3,
    right: Vec3,
    up: Vec3,
    color: Vec4,
) -> Vec<Line> {
    let mut lines = Vec::with_capacity(16);
    let direction = nalgebra_glm::normalize(&forward);
    let arrow_end = position + direction * ARROW_LENGTH;
    lines.push(Line {
        start: position,
        end: arrow_end,
        color,
    });
    let head_back = arrow_end - direction * ARROW_HEAD_SIZE;
    let right_normal = nalgebra_glm::normalize(&right) * (ARROW_HEAD_SIZE * 0.5);
    let up_normal = nalgebra_glm::normalize(&up) * (ARROW_HEAD_SIZE * 0.5);
    lines.push(Line {
        start: arrow_end,
        end: head_back + right_normal,
        color,
    });
    lines.push(Line {
        start: arrow_end,
        end: head_back - right_normal,
        color,
    });
    lines.push(Line {
        start: arrow_end,
        end: head_back + up_normal,
        color,
    });
    lines.push(Line {
        start: arrow_end,
        end: head_back - up_normal,
        color,
    });
    let parallel_offset = 0.6;
    for (offset_right, offset_up) in [
        (parallel_offset, 0.0),
        (-parallel_offset, 0.0),
        (0.0, parallel_offset),
        (0.0, -parallel_offset),
    ] {
        let offset = right * offset_right + up * offset_up;
        lines.push(Line {
            start: position + offset,
            end: position + offset + direction * ARROW_LENGTH,
            color,
        });
    }
    lines
}

fn spot_light_lines(
    apex: Vec3,
    forward: Vec3,
    right: Vec3,
    up: Vec3,
    outer_angle: f32,
    range: f32,
    color: Vec4,
) -> Vec<Line> {
    let mut lines = Vec::with_capacity(CONE_SEGMENTS + 8);
    let direction = nalgebra_glm::normalize(&forward);
    let cone_height = range;
    let cone_radius = cone_height * outer_angle.tan();
    let cone_center = apex + direction * cone_height;
    let mut prev = cone_perimeter_point(cone_center, right, up, cone_radius, 0.0);
    for index in 1..=CONE_SEGMENTS {
        let theta = (index as f32 / CONE_SEGMENTS as f32) * std::f32::consts::TAU;
        let next = cone_perimeter_point(cone_center, right, up, cone_radius, theta);
        lines.push(Line {
            start: prev,
            end: next,
            color,
        });
        prev = next;
    }
    let spokes = 4;
    for index in 0..spokes {
        let theta = (index as f32 / spokes as f32) * std::f32::consts::TAU;
        let perimeter = cone_perimeter_point(cone_center, right, up, cone_radius, theta);
        lines.push(Line {
            start: apex,
            end: perimeter,
            color,
        });
    }
    lines.push(Line {
        start: apex,
        end: cone_center,
        color,
    });
    lines
}

fn push_circle(
    lines: &mut Vec<Line>,
    center: Vec3,
    axis_a: Vec3,
    axis_b: Vec3,
    radius: f32,
    color: Vec4,
) {
    let mut prev = center + axis_a * radius;
    for index in 1..=SPHERE_SEGMENTS {
        let theta = (index as f32 / SPHERE_SEGMENTS as f32) * std::f32::consts::TAU;
        let next = center + axis_a * (theta.cos() * radius) + axis_b * (theta.sin() * radius);
        lines.push(Line {
            start: prev,
            end: next,
            color,
        });
        prev = next;
    }
}

fn cone_perimeter_point(center: Vec3, right: Vec3, up: Vec3, radius: f32, theta: f32) -> Vec3 {
    center + right * (theta.cos() * radius) + up * (theta.sin() * radius)
}