nightshade 0.8.2

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::gizmos::GizmoMode;
use crate::prelude::*;
use nalgebra_glm::{Vec2, Vec3};

pub const MIN_ENTITY_SCALE: f32 = 0.01;
pub const BASE_SENSITIVITY: f32 = 0.01;
const PLANE_AXIS_THRESHOLD: f32 = 1.05;
const SINGLE_AXIS_THRESHOLD: f32 = 0.99;

pub fn compute_drag_sensitivity(camera_distance: f32) -> f32 {
    BASE_SENSITIVITY * (camera_distance * 0.1).sqrt().max(0.5)
}

pub struct DragParams {
    pub mode: GizmoMode,
    pub camera_right: Vec3,
    pub camera_up: Vec3,
    pub mouse_delta: Vec2,
    pub sensitivity: f32,
}

pub fn determine_drag_mode(axis: Vec3) -> crate::ecs::gizmos::GizmoDragMode {
    use crate::ecs::gizmos::GizmoDragMode;

    if axis.x > PLANE_AXIS_THRESHOLD
        || axis.y > PLANE_AXIS_THRESHOLD
        || axis.z > PLANE_AXIS_THRESHOLD
    {
        let plane_normal = if axis.x > PLANE_AXIS_THRESHOLD && axis.y > PLANE_AXIS_THRESHOLD {
            nalgebra_glm::vec3(0.0, 0.0, 1.0)
        } else if axis.y > PLANE_AXIS_THRESHOLD && axis.z > PLANE_AXIS_THRESHOLD {
            nalgebra_glm::vec3(1.0, 0.0, 0.0)
        } else if axis.x > PLANE_AXIS_THRESHOLD && axis.z > PLANE_AXIS_THRESHOLD {
            nalgebra_glm::vec3(0.0, 1.0, 0.0)
        } else {
            return GizmoDragMode::Free;
        };
        GizmoDragMode::Plane(plane_normal)
    } else {
        let single_axis = if axis.x > SINGLE_AXIS_THRESHOLD {
            nalgebra_glm::vec3(1.0, 0.0, 0.0)
        } else if axis.y > SINGLE_AXIS_THRESHOLD {
            nalgebra_glm::vec3(0.0, 1.0, 0.0)
        } else if axis.z > SINGLE_AXIS_THRESHOLD {
            nalgebra_glm::vec3(0.0, 0.0, 1.0)
        } else {
            return GizmoDragMode::Free;
        };
        GizmoDragMode::Axis(single_axis)
    }
}

pub fn extract_normalized_axes(transform: &GlobalTransform) -> (Vec3, Vec3, Vec3) {
    let right = nalgebra_glm::normalize(&nalgebra_glm::vec3(
        transform.0[(0, 0)],
        transform.0[(1, 0)],
        transform.0[(2, 0)],
    ));
    let up = nalgebra_glm::normalize(&nalgebra_glm::vec3(
        transform.0[(0, 1)],
        transform.0[(1, 1)],
        transform.0[(2, 1)],
    ));
    let forward = nalgebra_glm::normalize(&nalgebra_glm::vec3(
        transform.0[(0, 2)],
        transform.0[(1, 2)],
        transform.0[(2, 2)],
    ));
    (right, up, forward)
}

pub fn rotate_vec3_by_global_transform(transform: &GlobalTransform, v: &Vec3) -> Vec3 {
    let (right, up, forward) = extract_normalized_axes(transform);
    right * v.x + up * v.y + forward * v.z
}

pub fn rotate_vec3_by_inverse_global_transform(transform: &GlobalTransform, v: &Vec3) -> Vec3 {
    let (right, up, forward) = extract_normalized_axes(transform);
    nalgebra_glm::vec3(
        nalgebra_glm::dot(&right, v),
        nalgebra_glm::dot(&up, v),
        nalgebra_glm::dot(&forward, v),
    )
}

pub fn world_to_local_movement(world: &World, entity: Entity, world_movement: Vec3) -> Vec3 {
    if let Some(parent_entity) = world.get_parent(entity).and_then(|p| p.0) {
        if let Some(parent_transform) = world.get_global_transform(parent_entity) {
            rotate_vec3_by_inverse_global_transform(parent_transform, &world_movement)
        } else {
            world_movement
        }
    } else {
        world_movement
    }
}

enum TransformDelta {
    Translation(Vec3),
    Rotation(Quat),
    Scale(Vec3),
}

fn apply_transform_delta(world: &mut World, target: Entity, delta: TransformDelta) {
    let Some(transform) = world.get_local_transform(target) else {
        return;
    };
    let mut new_transform = *transform;
    match delta {
        TransformDelta::Translation(movement) => {
            new_transform.translation += movement;
        }
        TransformDelta::Rotation(rotation) => {
            new_transform.rotation = rotation * new_transform.rotation;
        }
        TransformDelta::Scale(scale) => {
            new_transform.scale.x *= scale.x;
            new_transform.scale.y *= scale.y;
            new_transform.scale.z *= scale.z;
            new_transform.scale.x = new_transform.scale.x.max(MIN_ENTITY_SCALE);
            new_transform.scale.y = new_transform.scale.y.max(MIN_ENTITY_SCALE);
            new_transform.scale.z = new_transform.scale.z.max(MIN_ENTITY_SCALE);
        }
    }
    world.assign_local_transform(target, new_transform);
}

fn compute_translation_on_axis(
    world: &World,
    target: Entity,
    axis: Vec3,
    params: &DragParams,
) -> Vec3 {
    let world_axis = if let Some(entity_transform) = world.get_global_transform(target) {
        rotate_vec3_by_global_transform(entity_transform, &axis)
    } else {
        axis
    };
    let screen_movement =
        params.camera_right * params.mouse_delta.x - params.camera_up * params.mouse_delta.y;
    let movement_amount = nalgebra_glm::dot(&screen_movement, &world_axis) * params.sensitivity;
    let world_movement = world_axis * movement_amount;
    world_to_local_movement(world, target, world_movement)
}

fn compute_translation_on_plane(
    world: &World,
    target: Entity,
    plane_normal: Vec3,
    params: &DragParams,
) -> Vec3 {
    let world_plane_normal = if let Some(entity_transform) = world.get_global_transform(target) {
        rotate_vec3_by_global_transform(entity_transform, &plane_normal)
    } else {
        plane_normal
    };
    let screen_movement = params.camera_right * params.mouse_delta.x * params.sensitivity
        - params.camera_up * params.mouse_delta.y * params.sensitivity;
    let world_movement = screen_movement
        - world_plane_normal * nalgebra_glm::dot(&screen_movement, &world_plane_normal);
    world_to_local_movement(world, target, world_movement)
}

fn compute_free_translation(world: &World, target: Entity, params: &DragParams) -> Vec3 {
    let world_movement = params.camera_right * params.mouse_delta.x * params.sensitivity
        - params.camera_up * params.mouse_delta.y * params.sensitivity;
    world_to_local_movement(world, target, world_movement)
}

fn rotation_angle_for_axis(axis: Vec3, mouse_delta: Vec2, rotation_speed: f32) -> f32 {
    if axis.x > SINGLE_AXIS_THRESHOLD {
        -mouse_delta.y * rotation_speed
    } else if axis.y > SINGLE_AXIS_THRESHOLD {
        mouse_delta.x * rotation_speed
    } else {
        -mouse_delta.x * rotation_speed
    }
}

fn rotation_angle_for_plane(plane_normal: Vec3, mouse_delta: Vec2, rotation_speed: f32) -> f32 {
    if plane_normal.z.abs() > SINGLE_AXIS_THRESHOLD {
        mouse_delta.x * rotation_speed
    } else if plane_normal.x.abs() > SINGLE_AXIS_THRESHOLD {
        -mouse_delta.y * rotation_speed
    } else {
        mouse_delta.x * rotation_speed
    }
}

fn scale_factors_for_axis(axis: Vec3, scale_delta: f32) -> Vec3 {
    let factor = 1.0 + scale_delta;
    if axis.x > SINGLE_AXIS_THRESHOLD {
        Vec3::new(factor, 1.0, 1.0)
    } else if axis.y > SINGLE_AXIS_THRESHOLD {
        Vec3::new(1.0, factor, 1.0)
    } else {
        Vec3::new(1.0, 1.0, factor)
    }
}

fn scale_factors_for_plane(plane_normal: Vec3, scale_delta: f32) -> Vec3 {
    let factor = 1.0 + scale_delta;
    if plane_normal.z.abs() > SINGLE_AXIS_THRESHOLD {
        Vec3::new(factor, factor, 1.0)
    } else if plane_normal.x.abs() > SINGLE_AXIS_THRESHOLD {
        Vec3::new(1.0, factor, factor)
    } else {
        Vec3::new(factor, 1.0, factor)
    }
}

pub fn apply_axis_drag(world: &mut World, target: Entity, axis: Vec3, params: &DragParams) {
    let delta = match params.mode {
        GizmoMode::LocalTranslation | GizmoMode::GlobalTranslation => {
            TransformDelta::Translation(compute_translation_on_axis(world, target, axis, params))
        }
        GizmoMode::Rotation => {
            let rotation_speed = params.sensitivity * 2.0;
            let angle = rotation_angle_for_axis(axis, params.mouse_delta, rotation_speed);
            TransformDelta::Rotation(nalgebra_glm::quat_angle_axis(angle, &axis))
        }
        GizmoMode::Scale => {
            let scale_delta = (params.mouse_delta.x - params.mouse_delta.y) * params.sensitivity;
            TransformDelta::Scale(scale_factors_for_axis(axis, scale_delta))
        }
    };
    apply_transform_delta(world, target, delta);
}

pub fn apply_plane_drag(
    world: &mut World,
    target: Entity,
    plane_normal: Vec3,
    params: &DragParams,
) {
    let delta = match params.mode {
        GizmoMode::LocalTranslation | GizmoMode::GlobalTranslation => TransformDelta::Translation(
            compute_translation_on_plane(world, target, plane_normal, params),
        ),
        GizmoMode::Rotation => {
            let rotation_speed = params.sensitivity * 2.0;
            let angle = rotation_angle_for_plane(plane_normal, params.mouse_delta, rotation_speed);
            TransformDelta::Rotation(nalgebra_glm::quat_angle_axis(angle, &plane_normal))
        }
        GizmoMode::Scale => {
            let scale_delta = (params.mouse_delta.x - params.mouse_delta.y) * params.sensitivity;
            TransformDelta::Scale(scale_factors_for_plane(plane_normal, scale_delta))
        }
    };
    apply_transform_delta(world, target, delta);
}

pub fn apply_free_drag(world: &mut World, target: Entity, params: &DragParams) {
    let delta = match params.mode {
        GizmoMode::LocalTranslation | GizmoMode::GlobalTranslation => {
            TransformDelta::Translation(compute_free_translation(world, target, params))
        }
        GizmoMode::Rotation => {
            let rotation_speed = params.sensitivity * 2.0;
            let axis_x = nalgebra_glm::vec3(1.0, 0.0, 0.0);
            let axis_y = nalgebra_glm::vec3(0.0, 1.0, 0.0);
            let rotation_x =
                nalgebra_glm::quat_angle_axis(-params.mouse_delta.y * rotation_speed, &axis_x);
            let rotation_y =
                nalgebra_glm::quat_angle_axis(params.mouse_delta.x * rotation_speed, &axis_y);
            TransformDelta::Rotation(rotation_y * rotation_x)
        }
        GizmoMode::Scale => {
            let scale_delta = (params.mouse_delta.x - params.mouse_delta.y) * params.sensitivity;
            let factor = 1.0 + scale_delta;
            TransformDelta::Scale(Vec3::new(factor, factor, factor))
        }
    };
    apply_transform_delta(world, target, delta);
}