nightshade-editor 0.21.0

Interactive map editor for the Nightshade game engine
use crate::ecs::{EditorWorld, PushPullSession};
use crate::systems::input;
use nightshade::ecs::input::resources::MouseState;
use nightshade::prelude::*;

pub fn is_dragging(editor_world: &EditorWorld) -> bool {
    editor_world.resources.push_pull.session.is_some()
}

pub fn tick(editor_world: &mut EditorWorld, world: &mut World) {
    if !editor_world.resources.push_pull.active {
        if editor_world.resources.push_pull.session.is_some() {
            cancel_session(editor_world, world);
        }
        return;
    }

    let mouse = *nightshade::ecs::input::access::mouse_for_active(world);
    let in_viewport = input::mouse_in_active_viewport(world);
    let ui_block = input::ui_capturing(world);
    let left_pressed = mouse.state.contains(MouseState::LEFT_CLICKED);
    let just_pressed = mouse.state.contains(MouseState::LEFT_JUST_PRESSED);
    let just_released = mouse.state.contains(MouseState::LEFT_JUST_RELEASED);
    let escape = world
        .resources
        .input
        .keyboard
        .is_key_pressed(KeyCode::Escape);

    if escape {
        cancel_session(editor_world, world);
        return;
    }

    if editor_world.resources.push_pull.session.is_none() {
        if just_pressed && in_viewport && !ui_block {
            start_session(editor_world, world, mouse.position);
        } else if in_viewport && !ui_block {
            highlight_face(editor_world, world, mouse.position);
        } else {
            clear_highlight(world);
        }
        return;
    }

    apply_drag(editor_world, world, mouse.position);
    if just_released || !left_pressed {
        commit_session(editor_world, world);
    }
}

fn picked_face(
    editor_world: &EditorWorld,
    world: &World,
    mouse_position: Vec2,
) -> Option<PickingResult> {
    pick_entities(world, mouse_position, PickingOptions::default())
        .into_iter()
        .find(|result| {
            !editor_world
                .resources
                .editor_scene
                .is_scaffolding(result.entity)
        })
}

fn highlight_face(editor_world: &EditorWorld, world: &mut World, mouse_position: Vec2) {
    match picked_face(editor_world, world, mouse_position) {
        Some(result) => {
            world
                .resources
                .editor_selection
                .bounding_volume_selected_entity = Some(result.entity);
            world.resources.editor_selection.selected_entities.clear();
        }
        None => clear_highlight(world),
    }
}

fn clear_highlight(world: &mut World) {
    world
        .resources
        .editor_selection
        .bounding_volume_selected_entity = None;
    world.resources.editor_selection.selected_entities.clear();
}

fn camera_forward(world: &World) -> Vec3 {
    world
        .resources
        .active_camera
        .and_then(|camera| world.core.get_global_transform(camera))
        .map(|transform| transform.forward_vector())
        .unwrap_or_else(|| Vec3::new(0.0, 0.0, -1.0))
}

fn start_session(editor_world: &mut EditorWorld, world: &mut World, mouse_position: Vec2) {
    let Some(result) = picked_face(editor_world, world, mouse_position) else {
        return;
    };
    let entity = result.entity;
    let Some(bounding_volume) = world.core.get_bounding_volume(entity) else {
        return;
    };
    let Some(global) = world.core.get_global_transform(entity) else {
        return;
    };
    let obb = bounding_volume.transform(&global.0).obb;
    let inverse = obb.orientation.conjugate();
    let local = nalgebra_glm::quat_rotate_vec3(&inverse, &(result.world_position - obb.center));
    let ratio = Vec3::new(
        local.x / obb.half_extents.x.max(1.0e-6),
        local.y / obb.half_extents.y.max(1.0e-6),
        local.z / obb.half_extents.z.max(1.0e-6),
    );
    let axis = if ratio.x.abs() >= ratio.y.abs() && ratio.x.abs() >= ratio.z.abs() {
        0
    } else if ratio.y.abs() >= ratio.z.abs() {
        1
    } else {
        2
    };
    let sign = if ratio[axis] >= 0.0 { 1.0 } else { -1.0 };
    let Some(local_transform) = world.core.get_local_transform(entity).copied() else {
        return;
    };
    let scale_axis = local_transform.scale[axis];
    let unit_half = if scale_axis.abs() > 1.0e-6 {
        obb.half_extents[axis] / scale_axis
    } else {
        0.5
    };
    let center = obb.center[axis];
    let half = obb.half_extents[axis];
    let Some(uuid) = editor_world.resources.editor_scene.uuid_for(entity) else {
        return;
    };
    editor_world.resources.push_pull.session = Some(PushPullSession {
        entity,
        uuid,
        axis,
        sign,
        fixed_face: center - sign * half,
        unit_half,
        plane_point: result.world_position,
        plane_normal: camera_forward(world),
        start_face: center + sign * half,
        original_local: local_transform,
    });
    world
        .resources
        .editor_selection
        .bounding_volume_selected_entity = Some(entity);
    world.resources.editor_selection.selected_entities.clear();
}

fn apply_drag(editor_world: &mut EditorWorld, world: &mut World, mouse_position: Vec2) {
    let Some(session) = editor_world.resources.push_pull.session.clone() else {
        return;
    };
    let Some(ray) = PickingRay::from_screen_position(world, mouse_position) else {
        return;
    };
    let plane_distance = -nalgebra_glm::dot(&session.plane_normal, &session.plane_point);
    let Some(hit) = ray.intersect_plane(session.plane_normal, plane_distance) else {
        return;
    };
    let mut axis_direction = Vec3::zeros();
    axis_direction[session.axis] = session.sign;
    let delta = nalgebra_glm::dot(&(hit - session.plane_point), &axis_direction);
    let step = super::retained_ui::build::snap_step(editor_world);
    let new_face = snap_scalar(session.start_face + session.sign * delta, step);
    let size = ((new_face - session.fixed_face) * session.sign).max(step);
    let new_center = session.fixed_face + session.sign * size * 0.5;
    let new_scale = size * 0.5 / session.unit_half.max(1.0e-6);
    if let Some(transform) = world.core.get_local_transform_mut(session.entity) {
        transform.translation[session.axis] = new_center;
        transform.scale[session.axis] = new_scale;
    }
    mark_local_transform_dirty(world, session.entity);
}

fn snap_scalar(value: f32, step: f32) -> f32 {
    if step <= 0.0 {
        value
    } else {
        (value / step).round() * step
    }
}

fn commit_session(editor_world: &mut EditorWorld, world: &mut World) {
    let Some(session) = editor_world.resources.push_pull.session.take() else {
        return;
    };
    let Some(new_local) = world.core.get_local_transform(session.entity).copied() else {
        return;
    };
    if new_local.translation != session.original_local.translation
        || new_local.scale != session.original_local.scale
    {
        editor_world.resources.undo.push(
            crate::undo::UndoableOperation::TransformChanged {
                uuid: session.uuid,
                old: session.original_local,
                new: new_local,
            },
            "Push/pull",
        );
        crate::scene_writeback::sync_entity(
            &mut editor_world.resources.project,
            &editor_world.resources.editor_scene,
            world,
            session.entity,
        );
        editor_world.resources.ui_handles.inspector.dirty = true;
    }
}

fn cancel_session(editor_world: &mut EditorWorld, world: &mut World) {
    if let Some(session) = editor_world.resources.push_pull.session.take()
        && let Some(transform) = world.core.get_local_transform_mut(session.entity)
    {
        *transform = session.original_local;
        mark_local_transform_dirty(world, session.entity);
    }
    clear_highlight(world);
}