nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
use super::selection::EntitySelection;
use super::undo::UndoHistory;
use crate::ecs::gizmos::{GizmoDragMode, GizmoMode, GizmoState};
use crate::prelude::*;

#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum CoordinateSpace {
    #[default]
    World,
    Local,
}

#[derive(Clone, Default)]
pub struct MarqueeState {
    pub active: bool,
    pub start_position: egui::Pos2,
    pub current_position: egui::Pos2,
}

impl MarqueeState {
    pub fn start(&mut self, position: egui::Pos2) {
        self.active = true;
        self.start_position = position;
        self.current_position = position;
    }

    pub fn update(&mut self, position: egui::Pos2) {
        self.current_position = position;
    }

    pub fn finish(&mut self) -> Option<egui::Rect> {
        if self.active {
            self.active = false;
            Some(egui::Rect::from_two_pos(
                self.start_position,
                self.current_position,
            ))
        } else {
            None
        }
    }

    pub fn cancel(&mut self) {
        self.active = false;
    }

    pub fn rect(&self) -> Option<egui::Rect> {
        if self.active {
            Some(egui::Rect::from_two_pos(
                self.start_position,
                self.current_position,
            ))
        } else {
            None
        }
    }
}

#[derive(Clone)]
pub struct SnapSettings {
    pub translation_snap: f32,
    pub rotation_snap_degrees: f32,
    pub scale_snap: f32,
    pub enabled: bool,
}

impl Default for SnapSettings {
    fn default() -> Self {
        Self {
            translation_snap: 0.25,
            rotation_snap_degrees: 15.0,
            scale_snap: 0.1,
            enabled: false,
        }
    }
}

impl SnapSettings {
    pub fn snap_translation(&self, value: f32) -> f32 {
        if self.enabled && self.translation_snap > 0.0 {
            (value / self.translation_snap).round() * self.translation_snap
        } else {
            value
        }
    }

    pub fn snap_rotation(&self, degrees: f32) -> f32 {
        if self.enabled && self.rotation_snap_degrees > 0.0 {
            (degrees / self.rotation_snap_degrees).round() * self.rotation_snap_degrees
        } else {
            degrees
        }
    }

    pub fn snap_scale(&self, value: f32) -> f32 {
        if self.enabled && self.scale_snap > 0.0 {
            (value / self.scale_snap).round() * self.scale_snap
        } else {
            value
        }
    }
}

#[derive(Clone, Copy, PartialEq, Default)]
pub enum TransformOperation {
    #[default]
    None,
    Grab,
    Rotate,
    Scale,
}

#[derive(Clone, Copy, PartialEq, Default)]
pub enum TransformConstraint {
    #[default]
    None,
    X,
    Y,
    Z,
}

pub struct ModalTransformState {
    pub operation: TransformOperation,
    pub constraint: TransformConstraint,
    pub initial_transforms: std::collections::HashMap<Entity, LocalTransform>,
    pub start_mouse_pos: Vec2,
    pub accumulated_delta: Vec2,
    pub numeric_input: String,
    pub numeric_value: Option<f32>,
}

impl Default for ModalTransformState {
    fn default() -> Self {
        Self {
            operation: TransformOperation::None,
            constraint: TransformConstraint::None,
            initial_transforms: std::collections::HashMap::new(),
            start_mouse_pos: Vec2::zeros(),
            accumulated_delta: Vec2::zeros(),
            numeric_input: String::new(),
            numeric_value: None,
        }
    }
}

pub struct GizmoInteraction {
    pub active: Option<GizmoState>,
    pub mode: GizmoMode,
    pub hover_axis: Option<Vec3>,
    pub drag_mode: GizmoDragMode,
    pub camera_distance: f32,
    pub drag_initial_transforms: std::collections::HashMap<Entity, LocalTransform>,
}

impl GizmoInteraction {
    pub fn is_dragging(&self) -> bool {
        !matches!(self.drag_mode, GizmoDragMode::None)
    }
}

impl Default for GizmoInteraction {
    fn default() -> Self {
        Self {
            active: None,
            mode: GizmoMode::GlobalTranslation,
            hover_axis: None,
            drag_mode: GizmoDragMode::None,
            camera_distance: 10.0,
            drag_initial_transforms: std::collections::HashMap::new(),
        }
    }
}

#[derive(Default)]
pub struct TransformEdit {
    pub pending: Option<(Entity, LocalTransform)>,
    pub modal: ModalTransformState,
}

#[derive(Default)]
pub struct EditorContext {
    pub gizmo_interaction: GizmoInteraction,
    pub transform_edit: TransformEdit,
    pub selection: EntitySelection,
    pub marquee: MarqueeState,
    pub coordinate_space: CoordinateSpace,
    pub snap_settings: SnapSettings,
    pub undo_history: UndoHistory,
}

impl EditorContext {
    pub fn gizmo_root(&self) -> Option<Entity> {
        self.gizmo_interaction.active.as_ref().map(|g| g.root)
    }

    pub fn capture_selection_transforms(
        &self,
        world: &World,
    ) -> std::collections::HashMap<Entity, LocalTransform> {
        let mut transforms = std::collections::HashMap::new();
        for &entity in self.selection.iter() {
            if let Some(transform) = world.get_local_transform(entity) {
                transforms.insert(entity, *transform);
            }
        }
        transforms
    }

    pub fn begin_selection_transform_tracking(&mut self, world: &World) {
        if self.selection.len() == 1
            && let Some(&entity) = self.selection.iter().next()
            && let Some(transform) = world.get_local_transform(entity)
        {
            super::undo::begin_transform_change(
                &mut self.transform_edit.pending,
                entity,
                *transform,
            );
        }
    }

    pub fn commit_selection_transforms(
        &mut self,
        world: &World,
        initial_transforms: std::collections::HashMap<Entity, LocalTransform>,
        description: &str,
    ) -> bool {
        let count = self.selection.len();
        if count == 1 {
            if let Some(entity) = self.selection.primary()
                && let Some(transform) = world.get_local_transform(entity)
            {
                super::undo::commit_transform_change(
                    &mut self.transform_edit.pending,
                    &mut self.undo_history,
                    entity,
                    *transform,
                );
                return true;
            }
            false
        } else if count > 1 {
            let transforms: Vec<(Entity, LocalTransform, LocalTransform)> = self
                .selection
                .iter()
                .filter_map(|&entity| {
                    let old = initial_transforms.get(&entity)?;
                    let new = world.get_local_transform(entity)?;
                    Some((entity, *old, *new))
                })
                .collect();
            super::undo::push_bulk_transform_change(
                &mut self.undo_history,
                transforms,
                description,
            );
            true
        } else {
            false
        }
    }
}