nightshade-api 0.53.0

Procedural high level API for the nightshade game engine
Documentation
//! A tiny scene inspector built on the read-back facade: pick a shape, read its
//! `describe_entity` view and bounds into a property grid, outline the scene in a
//! tree view, frame the selection, and undo or redo a visibility edit.

use nightshade_api::prelude::*;
use std::collections::HashSet;

struct Inspector {
    shapes: Vec<Entity>,
    selected: Option<Entity>,
    name_cell: Entity,
    id_cell: Entity,
    position_cell: Entity,
    mesh_cell: Entity,
    children_cell: Entity,
    bounds_cell: Entity,
    hud: Entity,
    stack: UndoStack,
}

fn main() {
    run(setup, update).unwrap();
}

fn setup(world: &mut World) -> Inspector {
    set_window_title(world, "Inspector");
    set_background(world, Background::Sky);
    show_grid(world, true);

    spawn_floor(world, 12.0);

    let mut shapes = Vec::new();
    let crate_box = spawn_cube(world, vec3(-3.0, 0.6, 0.0));
    set_color(world, crate_box, ORANGE);
    set_name(world, crate_box, "Crate");
    shapes.push(crate_box);

    let ball = spawn_sphere(world, vec3(0.0, 0.6, 0.0));
    set_color(world, ball, TEAL);
    set_name(world, ball, "Ball");
    shapes.push(ball);

    let pillar = spawn_cylinder(world, vec3(3.0, 0.6, 0.0));
    set_color(world, pillar, PURPLE);
    set_name(world, pillar, "Pillar");
    shapes.push(pillar);

    for &shape in &shapes {
        set_visible(world, shape, true);
    }

    let panel = spawn_panel(world, ScreenAnchor::TopRight, 320.0, 280.0);
    panel_heading(world, panel, "Selection");
    let grid = panel_property_grid(world, panel, 90.0);
    let name_cell = property_value(world, grid, "Name");
    let id_cell = property_value(world, grid, "Id");
    let position_cell = property_value(world, grid, "Position");
    let mesh_cell = property_value(world, grid, "Mesh");
    let children_cell = property_value(world, grid, "Children");
    let bounds_cell = property_value(world, grid, "Bounds");

    let tree_panel = spawn_panel(world, ScreenAnchor::BottomRight, 320.0, 220.0);
    panel_heading(world, tree_panel, "Scene");
    build_tree(world, tree_panel);

    spawn_text(
        world,
        "L click select  R click pick-behind  F frame  V visible  N rename  Z undo  Y redo",
        ScreenAnchor::BottomLeft,
    );
    let hud = spawn_text(world, "No selection", ScreenAnchor::TopLeft);

    Inspector {
        shapes,
        selected: None,
        name_cell,
        id_cell,
        position_cell,
        mesh_cell,
        children_cell,
        bounds_cell,
        hud,
        stack: UndoStack::new(),
    }
}

fn property_value(world: &mut World, grid: Entity, label: &str) -> Entity {
    let cell = panel_property_row(world, grid, label);
    panel_label(world, cell, "-")
}

fn build_tree(world: &mut World, panel: Entity) {
    let tree_view = panel_tree_view(world, panel, false);
    let content = tree_content(world, tree_view);
    let mut first_root: Option<Entity> = None;
    for row in scene_tree(world) {
        let node = tree_node(
            world,
            tree_view,
            content,
            &row.name,
            row.depth as usize,
            row.id as u64,
        );
        if row.depth == 0 && first_root.is_none() {
            first_root = Some(node);
        }
    }
    if let Some(node) = first_root {
        set_tree_node_expanded(world, node, true);
    }
}

fn update(world: &mut World, inspector: &mut Inspector) {
    if let Some(entity) = clicked_entity(world)
        && inspector.shapes.contains(&entity)
    {
        inspector.selected = Some(entity);
    }

    if mouse_clicked(world, MouseButton::Right) {
        let mut exclude = HashSet::new();
        if let Some(current) = inspector.selected {
            exclude.insert(current);
        }
        if let Some(behind) = pick_excluding(world, mouse_position(world), &exclude)
            && inspector.shapes.contains(&behind)
        {
            inspector.selected = Some(behind);
        }
    }

    let Some(selected) = inspector.selected else {
        set_text(world, inspector.hud, "No selection");
        return;
    };

    if key_pressed(world, KeyCode::KeyF) {
        frame_entities(world, &[selected]);
    }

    if key_pressed(world, KeyCode::KeyN) {
        let renamed = format!("{} (edited)", name(world, selected));
        set_name(world, selected, &renamed);
    }

    if key_pressed(world, KeyCode::KeyV) {
        let visible = describe_entity(world, selected)
            .and_then(|view| view.visibility)
            .unwrap_or(true);
        inspector
            .stack
            .capture(world, selected, SnapshotKind::Visibility);
        set_visible(world, selected, !visible);
        inspector.stack.commit();
    }

    if key_pressed(world, KeyCode::KeyZ) {
        inspector.stack.undo(world);
    }
    if key_pressed(world, KeyCode::KeyY) {
        inspector.stack.redo(world);
    }

    refresh_grid(world, inspector, selected);

    let visible = describe_entity(world, selected)
        .and_then(|view| view.visibility)
        .unwrap_or(true);
    let root_count = roots(world).len();
    set_text(
        world,
        inspector.hud,
        &format!(
            "{} | visible {} | roots {} | undo {} redo {}",
            name(world, selected),
            visible,
            root_count,
            inspector.stack.can_undo(),
            inspector.stack.can_redo(),
        ),
    );
}

fn refresh_grid(world: &mut World, inspector: &Inspector, selected: Entity) {
    let Some(view) = describe_entity(world, selected) else {
        return;
    };
    set_panel_text(world, inspector.name_cell, &view.name);
    set_panel_text(world, inspector.id_cell, &format!("{}", view.id));
    set_panel_text(
        world,
        inspector.position_cell,
        &format!(
            "{:.1}, {:.1}, {:.1}",
            view.translation[0], view.translation[1], view.translation[2]
        ),
    );
    set_panel_text(
        world,
        inspector.mesh_cell,
        view.mesh.as_deref().unwrap_or("none"),
    );
    set_panel_text(
        world,
        inspector.children_cell,
        &format!("{}", children(world, selected).len()),
    );
    let radius = bounds(world, selected)
        .map(|bound| bound.radius)
        .unwrap_or(0.0);
    set_panel_text(world, inspector.bounds_cell, &format!("r {radius:.2}"));
}