nightshade-editor 0.29.0

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

const FRUSTUM_DEPTH: f32 = 1.4;
const FRUSTUM_HALF_WIDTH: f32 = 0.5;
const FRUSTUM_HALF_HEIGHT: f32 = 0.34;
const PROXY_SIZE: f32 = 0.34;
const PROXY_MATERIAL: &str = "editor_camera_gizmo";

const COLOR_IDLE: Vec4 = Vec4::new(0.28, 0.72, 1.0, 1.0);
const COLOR_SELECTED: Vec4 = Vec4::new(1.0, 0.82, 0.2, 1.0);

/// Draws a clickable visual for every scene camera: a wire frustum plus a small
/// pickable proxy cube standing in for the camera, so a click selects the
/// camera entity (the camera itself has no mesh to hit). Mirrors the engine
/// light-gizmo pattern for the lines, and the proxy is editor scaffolding so it
/// is never written to the scene.
pub fn update(editor_world: &mut EditorWorld, world: &mut World) {
    let editor_camera = editor_world.resources.camera.camera_entity;
    let cameras: Vec<Entity> = {
        let editor_scene = &editor_world.resources.editor_scene;
        world
            .core
            .query_entities(CAMERA)
            .filter(|entity| {
                Some(*entity) != editor_camera && !editor_scene.is_scaffolding(*entity)
            })
            .collect()
    };

    despawn_stale_proxies(editor_world, world, &cameras);
    ensure_proxy_material(world);
    sync_proxies(editor_world, world, &cameras);

    let selected = editor_world.resources.ui.selected_entity;
    let lines_entity = ensure_lines_entity(editor_world, world);
    let mut lines = Vec::new();
    for &camera in &cameras {
        if let Some(transform) = world.core.get_global_transform(camera) {
            push_frustum(&mut lines, transform, selected == Some(camera));
        }
    }
    world.core.set_lines(lines_entity, Lines::new(lines));
}

fn despawn_stale_proxies(editor_world: &mut EditorWorld, world: &mut World, cameras: &[Entity]) {
    let stale: Vec<Entity> = editor_world
        .resources
        .camera_gizmos
        .camera_to_proxy
        .keys()
        .filter(|camera| !cameras.contains(camera))
        .copied()
        .collect();
    for camera in stale {
        if let Some(proxy) = editor_world
            .resources
            .camera_gizmos
            .camera_to_proxy
            .remove(&camera)
        {
            editor_world
                .resources
                .editor_scene
                .unregister_scaffolding(proxy);
            despawn_recursive_immediate(world, proxy);
        }
    }
}

fn sync_proxies(editor_world: &mut EditorWorld, world: &mut World, cameras: &[Entity]) {
    for &camera in cameras {
        let position = world
            .core
            .get_global_transform(camera)
            .map(|transform| transform.translation())
            .unwrap_or_else(Vec3::zeros);

        let existing = editor_world
            .resources
            .camera_gizmos
            .camera_to_proxy
            .get(&camera)
            .copied()
            .filter(|proxy| world.core.entity_has_components(*proxy, RENDER_MESH));

        let proxy = match existing {
            Some(proxy) => proxy,
            None => {
                let proxy = spawn_mesh(
                    world,
                    "Cube",
                    position,
                    Vec3::new(PROXY_SIZE, PROXY_SIZE, PROXY_SIZE),
                );
                world
                    .core
                    .set_material_ref(proxy, MaterialRef::new(PROXY_MATERIAL));
                editor_world
                    .resources
                    .editor_scene
                    .register_scaffolding(proxy);
                editor_world
                    .resources
                    .camera_gizmos
                    .camera_to_proxy
                    .insert(camera, proxy);
                proxy
            }
        };

        if let Some(transform) = world.core.get_local_transform_mut(proxy) {
            transform.translation = position;
        }
        mark_local_transform_dirty(world, proxy);
    }
}

fn ensure_lines_entity(editor_world: &mut EditorWorld, world: &mut World) -> Entity {
    if let Some(entity) = editor_world.resources.camera_gizmos.lines
        && 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.camera_gizmos.lines = Some(entity);
    entity
}

fn ensure_proxy_material(world: &mut World) {
    if world
        .resources
        .assets
        .material_registry
        .registry
        .name_to_index
        .contains_key(PROXY_MATERIAL)
    {
        return;
    }
    material_registry_insert(
        &mut world.resources.assets.material_registry,
        PROXY_MATERIAL.to_string(),
        Material {
            base_color: [0.28, 0.72, 1.0, 1.0],
            emissive_factor: [0.12, 0.36, 0.55],
            emissive_strength: 2.0,
            unlit: true,
            ..Default::default()
        },
    );
    if let Some(&index) = world
        .resources
        .assets
        .material_registry
        .registry
        .name_to_index
        .get(PROXY_MATERIAL)
    {
        registry_add_reference(
            &mut world.resources.assets.material_registry.registry,
            index,
        );
    }
}

fn push_frustum(lines: &mut Vec<Line>, transform: &GlobalTransform, selected: bool) {
    let position = transform.translation();
    let forward = nalgebra_glm::normalize(&transform.forward_vector());
    let right = nalgebra_glm::normalize(&transform.right_vector());
    let up = nalgebra_glm::normalize(&transform.up_vector());
    let color = if selected { COLOR_SELECTED } else { COLOR_IDLE };

    let far_center = position + forward * FRUSTUM_DEPTH;
    let half_width = right * FRUSTUM_HALF_WIDTH;
    let half_height = up * FRUSTUM_HALF_HEIGHT;
    let corners = [
        far_center + half_width + half_height,
        far_center - half_width + half_height,
        far_center - half_width - half_height,
        far_center + half_width - half_height,
    ];

    for corner in corners {
        lines.push(Line {
            start: position,
            end: corner,
            color,
        });
    }
    for index in 0..4 {
        lines.push(Line {
            start: corners[index],
            end: corners[(index + 1) % 4],
            color,
        });
    }

    let top_mid = (corners[0] + corners[1]) * 0.5;
    let peak = top_mid + up * (FRUSTUM_HALF_HEIGHT * 0.7);
    lines.push(Line {
        start: corners[0],
        end: peak,
        color,
    });
    lines.push(Line {
        start: corners[1],
        end: peak,
        color,
    });
}