nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
use crate::editor::context::{self, EditorContext};
use crate::editor::gizmo::drag_math::{
    BASE_SENSITIVITY, MIN_ENTITY_SCALE, compute_drag_sensitivity, world_to_local_movement,
};
use crate::editor::input::is_ctrl_pressed;
use crate::prelude::*;
use nalgebra_glm::{Vec2, Vec3};

fn constraint_to_grab_vector(constraint: context::TransformConstraint, value: Vec3) -> Vec3 {
    match constraint {
        context::TransformConstraint::None => value,
        context::TransformConstraint::X => Vec3::new(value.x, 0.0, 0.0),
        context::TransformConstraint::Y => Vec3::new(0.0, value.y, 0.0),
        context::TransformConstraint::Z => Vec3::new(0.0, 0.0, value.z),
    }
}

fn constraint_to_rotation_axis(constraint: context::TransformConstraint) -> Vec3 {
    match constraint {
        context::TransformConstraint::None | context::TransformConstraint::Y => Vec3::y(),
        context::TransformConstraint::X => Vec3::x(),
        context::TransformConstraint::Z => Vec3::z(),
    }
}

fn constraint_to_scale_delta(constraint: context::TransformConstraint, scale_factor: f32) -> Vec3 {
    match constraint {
        context::TransformConstraint::None => Vec3::new(scale_factor, scale_factor, scale_factor),
        context::TransformConstraint::X => Vec3::new(scale_factor, 1.0, 1.0),
        context::TransformConstraint::Y => Vec3::new(1.0, scale_factor, 1.0),
        context::TransformConstraint::Z => Vec3::new(1.0, 1.0, scale_factor),
    }
}

pub fn start_modal_transform(
    context: &mut EditorContext,
    world: &mut World,
    operation: context::TransformOperation,
) {
    if context.selection.is_empty() {
        return;
    }

    if context.selection.primary().is_none() {
        return;
    }

    context.transform_edit.modal.operation = operation;
    context.transform_edit.modal.constraint = context::TransformConstraint::None;
    context.transform_edit.modal.start_mouse_pos = world.resources.input.mouse.position;
    context.transform_edit.modal.accumulated_delta = Vec2::zeros();
    context.transform_edit.modal.numeric_input.clear();
    context.transform_edit.modal.numeric_value = None;

    context.transform_edit.modal.initial_transforms = context.capture_selection_transforms(world);
    context.begin_selection_transform_tracking(world);
}

pub fn confirm_modal_transform(context: &mut EditorContext, world: &mut World) -> bool {
    let initial_transforms = std::mem::take(&mut context.transform_edit.modal.initial_transforms);
    let modified =
        context.commit_selection_transforms(world, initial_transforms, "Transform selection");
    context.transform_edit.modal = context::ModalTransformState::default();
    modified
}

pub fn cancel_modal_transform(context: &mut EditorContext, world: &mut World) {
    for (entity, transform) in &context.transform_edit.modal.initial_transforms {
        world.set_local_transform(*entity, *transform);
        mark_local_transform_dirty(world, *entity);
    }
    context.transform_edit.modal = context::ModalTransformState::default();
}

pub fn update_modal_transform(context: &mut EditorContext, world: &mut World) -> bool {
    if context.transform_edit.modal.operation == context::TransformOperation::None {
        return false;
    }

    if context.transform_edit.modal.numeric_value.is_some() {
        return false;
    }

    if context.transform_edit.modal.initial_transforms.is_empty() {
        context.transform_edit.modal = context::ModalTransformState::default();
        return false;
    }

    let mouse = &world.resources.input.mouse;
    let mouse_delta = mouse.position - context.transform_edit.modal.start_mouse_pos;

    if mouse
        .state
        .contains(crate::prelude::MouseState::LEFT_JUST_PRESSED)
    {
        return confirm_modal_transform(context, world);
    }
    if mouse
        .state
        .contains(crate::prelude::MouseState::RIGHT_JUST_PRESSED)
    {
        cancel_modal_transform(context, world);
        return false;
    }

    let constraint = context.transform_edit.modal.constraint;
    let operation = context.transform_edit.modal.operation;

    let ctrl_pressed = is_ctrl_pressed(world);

    let Some(camera_entity) = world.resources.active_camera else {
        return false;
    };
    let Some(camera_transform) = world.get_global_transform(camera_entity) else {
        return false;
    };

    let (camera_right, camera_up, _) = super::drag_math::extract_normalized_axes(camera_transform);

    for (&entity, &initial) in &context.transform_edit.modal.initial_transforms {
        match operation {
            context::TransformOperation::Grab => {
                let sensitivity =
                    compute_drag_sensitivity(context.gizmo_interaction.camera_distance);
                let mut world_movement = camera_right * mouse_delta.x * sensitivity
                    - camera_up * mouse_delta.y * sensitivity;

                if ctrl_pressed {
                    world_movement.x = context.snap_settings.snap_translation(world_movement.x);
                    world_movement.y = context.snap_settings.snap_translation(world_movement.y);
                    world_movement.z = context.snap_settings.snap_translation(world_movement.z);
                }

                let constrained_movement = constraint_to_grab_vector(constraint, world_movement);
                let local_movement = world_to_local_movement(world, entity, constrained_movement);

                let mut new_transform = initial;
                new_transform.translation += local_movement;
                world.set_local_transform(entity, new_transform);
                mark_local_transform_dirty(world, entity);
            }
            context::TransformOperation::Rotate => {
                let sensitivity = BASE_SENSITIVITY;
                let mut angle = mouse_delta.x * sensitivity;

                if ctrl_pressed {
                    let degrees = angle.to_degrees();
                    let snapped_degrees = context.snap_settings.snap_rotation(degrees);
                    angle = snapped_degrees.to_radians();
                }

                let axis = constraint_to_rotation_axis(constraint);
                let rotation = nalgebra_glm::quat_angle_axis(angle, &axis);
                let mut new_transform = initial;
                new_transform.rotation = rotation * initial.rotation;
                world.set_local_transform(entity, new_transform);
                mark_local_transform_dirty(world, entity);
            }
            context::TransformOperation::Scale => {
                let sensitivity = BASE_SENSITIVITY;
                let mut scale_factor = 1.0 + mouse_delta.x * sensitivity;
                scale_factor = scale_factor.max(MIN_ENTITY_SCALE);

                if ctrl_pressed {
                    scale_factor = context.snap_settings.snap_scale(scale_factor);
                }

                let scale_delta = constraint_to_scale_delta(constraint, scale_factor);

                let mut new_transform = initial;
                new_transform.scale = Vec3::new(
                    initial.scale.x * scale_delta.x,
                    initial.scale.y * scale_delta.y,
                    initial.scale.z * scale_delta.z,
                );
                world.set_local_transform(entity, new_transform);
                mark_local_transform_dirty(world, entity);
            }
            context::TransformOperation::None => {}
        }
    }

    false
}

pub fn handle_numeric_input_for_transform(
    context: &mut EditorContext,
    key_code: KeyCode,
    world: &mut World,
) -> bool {
    let char_opt = match key_code {
        KeyCode::Digit0 | KeyCode::Numpad0 => Some('0'),
        KeyCode::Digit1 | KeyCode::Numpad1 => Some('1'),
        KeyCode::Digit2 | KeyCode::Numpad2 => Some('2'),
        KeyCode::Digit3 | KeyCode::Numpad3 => Some('3'),
        KeyCode::Digit4 | KeyCode::Numpad4 => Some('4'),
        KeyCode::Digit5 | KeyCode::Numpad5 => Some('5'),
        KeyCode::Digit6 | KeyCode::Numpad6 => Some('6'),
        KeyCode::Digit7 | KeyCode::Numpad7 => Some('7'),
        KeyCode::Digit8 | KeyCode::Numpad8 => Some('8'),
        KeyCode::Digit9 | KeyCode::Numpad9 => Some('9'),
        KeyCode::Period | KeyCode::NumpadDecimal => Some('.'),
        KeyCode::Minus | KeyCode::NumpadSubtract => Some('-'),
        KeyCode::Backspace => {
            context.transform_edit.modal.numeric_input.pop();
            context.transform_edit.modal.numeric_value =
                context.transform_edit.modal.numeric_input.parse().ok();
            return false;
        }
        KeyCode::Enter | KeyCode::NumpadEnter => {
            apply_numeric_transform(context, world);
            return confirm_modal_transform(context, world);
        }
        _ => None,
    };

    if let Some(character) = char_opt {
        if character == '-' && !context.transform_edit.modal.numeric_input.is_empty() {
            return false;
        }
        if character == '.' && context.transform_edit.modal.numeric_input.contains('.') {
            return false;
        }

        context.transform_edit.modal.numeric_input.push(character);
        context.transform_edit.modal.numeric_value =
            context.transform_edit.modal.numeric_input.parse().ok();

        if context.transform_edit.modal.numeric_value.is_some() {
            apply_numeric_transform(context, world);
        }
    }
    false
}

fn apply_numeric_transform(context: &mut EditorContext, world: &mut World) {
    let Some(value) = context.transform_edit.modal.numeric_value else {
        return;
    };

    let operation = context.transform_edit.modal.operation;
    let constraint = context.transform_edit.modal.constraint;

    for (entity, initial) in &context.transform_edit.modal.initial_transforms {
        let mut new_transform = *initial;

        match operation {
            context::TransformOperation::Grab => {
                let movement =
                    constraint_to_grab_vector(constraint, Vec3::new(value, value, value));
                new_transform.translation = initial.translation + movement;
            }
            context::TransformOperation::Rotate => {
                let angle = value.to_radians();
                let axis = constraint_to_rotation_axis(constraint);
                let rotation = nalgebra_glm::quat_angle_axis(angle, &axis);
                new_transform.rotation = rotation * initial.rotation;
            }
            context::TransformOperation::Scale => {
                let scale_factor = value.max(MIN_ENTITY_SCALE);
                let scale_delta = constraint_to_scale_delta(constraint, scale_factor);
                new_transform.scale = Vec3::new(
                    initial.scale.x * scale_delta.x,
                    initial.scale.y * scale_delta.y,
                    initial.scale.z * scale_delta.z,
                );
            }
            context::TransformOperation::None => {}
        }

        world.set_local_transform(*entity, new_transform);
        mark_local_transform_dirty(world, *entity);
    }
}