vertra 0.2.0

A cross-platform graphics editor built with Rust and WebAssembly.
Documentation
//! Data types and event enums for the editor subsystem.

use serde::{Deserialize, Serialize};
use winit::keyboard::KeyCode;

use crate::geometry::Geometry;
use crate::objects::Object;

/// A snapshot of a selected scene object exposed by the editor inspector.
///
/// Returned by [`crate::scene::Scene::inspector`] and passed to callbacks.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InspectorData {
    pub id:            usize,
    pub name:          String,
    pub str_id:        String,
    pub position:      [f32; 3],
    pub rotation_deg:  [f32; 3],
    pub scale:         [f32; 3],
    pub color:         [f32; 4],
    pub geometry_type: Option<String>,
    pub texture_path:  Option<String>,
}

impl InspectorData {
    /// Build an [`InspectorData`] snapshot from a live [`Object`].
    pub fn from_object(id: usize, obj: &Object) -> Self {
        Self {
            id,
            name:          obj.name.clone(),
            str_id:        obj.str_id.clone(),
            position:      obj.transform.position,
            rotation_deg:  obj.transform.rotation,
            scale:         obj.transform.scale,
            color:         obj.color,
            geometry_type: obj.geometry.as_ref().map(geometry_type_name),
            texture_path:  obj.texture_path.clone(),
        }
    }
}

fn geometry_type_name(g: &Geometry) -> String {
    match g {
        Geometry::Cube { .. }    => "Cube",
        Geometry::Box { .. }     => "Box",
        Geometry::Plane { .. }   => "Plane",
        Geometry::Pyramid { .. } => "Pyramid",
        Geometry::Capsule { .. } => "Capsule",
        Geometry::Sphere { .. }  => "Sphere",
    }.to_string()
}

/// Holds the currently-selected object (if any) in the editor inspector.
#[derive(Debug, Clone, Default)]
pub struct Inspector {
    pub selected: Option<InspectorData>,
}

impl Inspector {
    /// Clear the current selection.
    pub fn clear(&mut self) { self.selected = None; }
    /// Returns `true` when an object is currently selected.
    pub fn has_selection(&self) -> bool { self.selected.is_some() }
}

/// Which world axis a gizmo drag is constrained to.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DragAxis { X, Y, Z }

/// Which transformation mode the gizmo is currently in.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum GizmoMode {
    /// Move objects along an axis (arrow + cone tips). Default mode.
    #[default]
    Translate,
    /// Rotate objects around an axis (ring gizmo).
    Rotate,
    /// Scale objects along an axis (arrow + cube tips).
    Scale,
}

/// Which kind of drag transform is currently active.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DragKind { Translate, Rotate, Scale }

/// Live state of an ongoing gizmo axis drag.
#[derive(Debug, Clone)]
pub struct DragState {
    /// ID of the primary object being transformed.
    pub object_id: usize,
    /// Axis the drag is constrained to.
    pub axis:      DragAxis,
    /// World-space gizmo centre captured at drag-start (stable, no jitter).
    pub center:    [f32; 3],
    /// Which transform operation is being applied.
    pub kind:      DragKind,
}

/// Raw platform-agnostic input event fed into the editor subsystem.
///
/// These map 1:1 to low-level platform events (key presses, mouse moves,
/// scroll deltas, etc.).  In most use-cases you do not construct these
/// manually — [`crate::window::Window`] converts winit events and dispatches
/// them automatically while editor mode is active.
///
/// For *state-change* notifications (e.g. "the gizmo mode just changed")
/// see [`EditorStateEvent`] and
/// [`Window::on_editor_event`](crate::window::Window::on_editor_event).
#[derive(Debug, Clone)]
pub enum EditorEvent {
    /// Raw mouse-motion delta from the OS device stream.
    MouseMotionDelta { dx: f32, dy: f32 },
    /// Absolute cursor position in window-space pixels.
    CursorMoved { x: f32, y: f32 },
    /// Mouse-button state change. Each field is `Some(true)` on press,
    /// `Some(false)` on release, and `None` when that button was not involved.
    MouseButton { left: Option<bool>, middle: Option<bool>, right: Option<bool> },
    /// Scroll-wheel delta (positive = scroll up / zoom in).
    Scroll { delta: f32 },
    /// Modifier-key state snapshot (sent on every modifier change).
    ModifiersChanged { alt: bool, ctrl: bool },
    /// Synthetic "focus on selection" event, typically raised by the **F** key.
    FocusKey,
    /// A physical keyboard key was pressed.
    KeyPressed(KeyCode),
    /// A physical keyboard key was released.
    KeyReleased(KeyCode),
}

/// High-level editor state-change event, fired *after* the editor has
/// processed input and its internal state has actually changed.
///
/// Unlike [`EditorEvent`] (raw platform input), `EditorStateEvent` is
/// generated by comparing the editor state before and after each frame and
/// only emitted when something meaningful transitions.  Register a handler
/// with [`Window::on_editor_event`](crate::window::Window::on_editor_event).
///
/// The callback receives both the event **and** a cloned [`Object`] that is
/// contextually relevant (the selected or dragged object), or `None`:
///
/// * [`GizmoModeChanged`](Self::GizmoModeChanged) — the currently selected object.
/// * [`DragStart`](Self::DragStart) — the object being dragged.
/// * [`DragEnd`](Self::DragEnd) — the object that was just being dragged.
/// * [`SelectionChanged`](Self::SelectionChanged) — the newly selected object, or `None` if deselected.
///
/// # Example
///
/// ```rust,ignore
/// use vertra::editor::{EditorStateEvent, GizmoMode};
///
/// Window::new(state)
///     .on_editor_event(|_state, _scene, ev, obj| {
///         let name = obj.as_ref().map(|o| o.name.as_str()).unwrap_or("–");
///         match ev {
///             EditorStateEvent::GizmoModeChanged(mode) =>
///                 println!("[Editor] Gizmo → {:?}  (obj: {name})", mode),
///             EditorStateEvent::DragStart { axis } =>
///                 println!("[Editor] Drag on {:?}  (obj: {name})", axis),
///             EditorStateEvent::DragEnd =>
///                 println!("[Editor] Drag ended  (obj: {name})"),
///             EditorStateEvent::SelectionChanged =>
///                 println!("[Editor] Selection → {name}"),
///         }
///     })
///     .create();
/// ```
#[derive(Debug, Clone, PartialEq)]
pub enum EditorStateEvent {
    /// The active gizmo mode changed (T / R / E keybinds).
    ///
    /// The callback's `obj` parameter is the currently selected object, if any.
    GizmoModeChanged(GizmoMode),

    /// The user started dragging a gizmo axis handle.
    ///
    /// The callback's `obj` parameter is the object being transformed.
    DragStart {
        /// The world axis being dragged.
        axis: DragAxis,
    },

    /// The user released a gizmo axis drag (mouse button up).
    ///
    /// The callback's `obj` parameter is the object that was being transformed.
    DragEnd,

    /// The editor selection changed — an object was clicked or deselected.
    ///
    /// The callback's `obj` parameter is the newly selected object, or `None`
    /// when the selection was cleared.
    SelectionChanged,
}

/// Per-frame raw input state maintained by the editor.
///
/// Updated on every [`EditorEvent`] by [`crate::editor::EditorState::process`]
/// and consumed during drag, orbit, and pan logic.
#[derive(Debug, Default)]
pub struct EditorInput {
    /// Whether the left mouse button is currently held down.
    pub left_down:   bool,
    /// Whether the middle mouse button is currently held down.
    pub middle_down: bool,
    /// Whether the right mouse button is currently held down.
    pub right_down:  bool,
    /// Whether the Alt modifier key is currently held.
    pub alt_held:    bool,
    /// Whether the Ctrl modifier key is currently held.
    pub ctrl_held:   bool,
    /// Current cursor X position in window-space pixels.
    pub cursor_x:    f32,
    /// Current cursor Y position in window-space pixels.
    pub cursor_y:    f32,
}