//! Retained-UI translation/scale gizmo overlay.
//!
//! Draws an axis-aligned X/Y/Z gizmo at the entity stored in
//! `world.resources.editor_selection.bounding_volume_selected_entity` and lets the
//! user drag axes to translate or scale that entity. Apps select which gizmo
//! to display by setting `world.resources.user_interface.gizmos.mode`.
//!
//! The system runs as part of the default frame schedule, after the retained
//! UI sub-schedule has built its frame, so the gizmo rects layer behind UI
//! panels but above the 3D scene.

use super::overlays_enabled::active_camera_overlays_enabled;
use super::state::{
    GizmoMode, GizmoPlanarScaleDrag, GizmoPlanarTranslationDrag, GizmoRotationDrag, GizmoScaleDrag,
    GizmoTranslationDrag,
};
use crate::ecs::input::resources::MouseState;
use crate::ecs::transform::commands::mark_local_transform_dirty;
use crate::ecs::window::resources::ViewportRect;
use crate::ecs::world::World;
use crate::render::wgpu::passes::geometry::{UiLayer, UiRect};
use nalgebra_glm::{Mat4, Vec2, Vec3, Vec4};

const ORIGIN_HANDLE_PX: f32 = 8.0;
const ORIGIN_HIT_THRESHOLD_PX: f32 = ORIGIN_HANDLE_PX * 0.7;
const Z_INDEX: i32 = 5_000;
const TRIANGLE_EFFECT_KIND: u32 = 8;
const SCALE_DRAG_SENSITIVITY: f32 = 1.0;
const MIN_SCALE_COMPONENT: f32 = 0.001;
const PLANE_HANDLE_INNER_FRACTION: f32 = 0.30;
const PLANE_HANDLE_OUTER_FRACTION: f32 = 0.55;
const ROTATION_RING_SEGMENTS: usize = 128;
const ROTATION_HIT_THRESHOLD_PX: f32 = 8.0;

const X_COLOR: Vec4 = Vec4::new(0.95, 0.30, 0.30, 1.0);
const Y_COLOR: Vec4 = Vec4::new(0.40, 0.95, 0.30, 1.0);
const Z_COLOR: Vec4 = Vec4::new(0.30, 0.55, 0.95, 1.0);
const HOVER_COLOR: Vec4 = Vec4::new(1.0, 0.85, 0.20, 1.0);
const YZ_PLANE_COLOR: Vec4 = Vec4::new(0.85, 0.30, 0.30, 0.45);
const XZ_PLANE_COLOR: Vec4 = Vec4::new(0.40, 0.85, 0.30, 0.45);
const XY_PLANE_COLOR: Vec4 = Vec4::new(0.30, 0.55, 0.85, 0.45);
const PLANE_HOVER_COLOR: Vec4 = Vec4::new(1.0, 0.85, 0.20, 0.65);

const AXIS_COLORS: [Vec4; 3] = [X_COLOR, Y_COLOR, Z_COLOR];
const PLANE_COLORS: [Vec4; 3] = [YZ_PLANE_COLOR, XZ_PLANE_COLOR, XY_PLANE_COLOR];

#[derive(Clone, Copy, PartialEq, Eq)]
enum DrawHead {
    Triangle,
    Box,
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum ActiveMode {
    LocalTranslation,
    GlobalTranslation,
    Scale,
    Rotation,
    CompositeLocal,
    CompositeGlobal,
}

#[derive(Clone, Copy)]
struct GizmoSizing {
    axis_screen_length_px: f32,
    shaft_thickness_px: f32,
    shaft_hover_thickness_px: f32,
    head_length_px: f32,
    head_thickness_px: f32,
    scale_box_px: f32,
    head_hit_extension_px: f32,
    scale_hit_extension_px: f32,
    hit_threshold_px: f32,
}

const DEFAULT_SIZING: GizmoSizing = GizmoSizing {
    axis_screen_length_px: 90.0,
    shaft_thickness_px: 3.0,
    shaft_hover_thickness_px: 4.5,
    head_length_px: 14.0,
    head_thickness_px: 10.0,
    scale_box_px: 12.0,
    head_hit_extension_px: 7.0,
    scale_hit_extension_px: 6.0,
    hit_threshold_px: 8.0,
};

const TRANSLATION_SIZING: GizmoSizing = GizmoSizing {
    axis_screen_length_px: 130.0,
    shaft_thickness_px: 5.0,
    shaft_hover_thickness_px: 7.0,
    head_length_px: 22.0,
    head_thickness_px: 16.0,
    scale_box_px: 12.0,
    head_hit_extension_px: 11.0,
    scale_hit_extension_px: 6.0,
    hit_threshold_px: 12.0,
};

fn sizing_for_mode(mode: ActiveMode) -> GizmoSizing {
    match mode {
        ActiveMode::LocalTranslation | ActiveMode::GlobalTranslation => TRANSLATION_SIZING,
        _ => DEFAULT_SIZING,
    }
}

#[derive(Clone, Copy)]
enum HoveredHandle {
    None,
    Origin,
    Axis(usize),
    Plane(usize),
    Ring(usize),
}

pub fn gizmo_overlay_system(world: &mut World) {
    world.resources.user_interface.hud_wants_pointer = false;

    if !world.resources.retained_ui.enabled {
        return;
    }

    draw_selection_marquee(world);

    if !active_camera_overlays_enabled(world) {
        clear_all_drags(world);
        return;
    }

    let mode = world.resources.user_interface.gizmos.mode;
    let active_mode = resolve_active_mode(mode);
    let sizing = sizing_for_mode(active_mode);
    clear_inactive_drags(world, active_mode);

    let Some(target) = world
        .resources
        .editor_selection
        .bounding_volume_selected_entity
    else {
        clear_all_drags(world);
        return;
    };
    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };
    if target == camera_entity {
        clear_all_drags(world);
        return;
    }

    let Some(target_transform) = world.core.get_global_transform(target) else {
        return;
    };
    let target_position = target_transform.translation();

    let Some(camera) = world.core.get_camera(camera_entity).copied() else {
        return;
    };
    let Some(camera_transform) = world.core.get_global_transform(camera_entity) else {
        return;
    };
    let camera_position = camera_transform.translation();
    let camera_forward = camera_transform.forward_vector();
    let view_matrix = camera_transform
        .0
        .try_inverse()
        .unwrap_or_else(Mat4::identity);

    let viewport = match world
        .resources
        .window
        .camera_tile_rects
        .get(&camera_entity)
        .copied()
        .or(world.resources.window.active_viewport_rect)
    {
        Some(rect) if rect.width > 0.0 && rect.height > 0.0 => rect,
        _ => return,
    };

    world.resources.user_interface.gizmos.current_clip = Some(crate::ecs::ui::types::Rect::new(
        viewport.x,
        viewport.y,
        viewport.width,
        viewport.height,
    ));

    let aspect = viewport.width / viewport.height;
    let projection_matrix = camera.projection.matrix_with_aspect(aspect);
    let view_projection = projection_matrix * view_matrix;
    let inverse_view_projection = view_projection.try_inverse().unwrap_or_else(Mat4::identity);

    let view_distance =
        nalgebra_glm::dot(&(target_position - camera_position), &camera_forward).max(0.001);
    let axis_world_length = world_length_for_screen_pixels(
        &camera.projection,
        view_distance,
        viewport.height,
        sizing.axis_screen_length_px,
    );

    let axis_directions = axis_directions_for_mode(active_mode, &target_transform.0);

    let Some(hit_origin_screen) = project(&view_projection, &viewport, target_position) else {
        world.resources.user_interface.gizmos.current_clip = None;
        return;
    };

    let mouse = crate::ecs::input::access::mouse_for_active(world);
    let mouse_position = mouse.position;
    let mouse_state = mouse.state;
    let mouse_in_viewport = viewport.contains(mouse_position);
    let pointer_over_panel = pointer_over_ui_panel(world);
    let just_pressed = mouse_state.contains(MouseState::LEFT_JUST_PRESSED);
    let just_released = mouse_state.contains(MouseState::LEFT_JUST_RELEASED);
    let held = mouse_state.contains(MouseState::LEFT_CLICKED);

    let mut hit_axis_ends: [Option<Vec2>; 3] = [None, None, None];
    for (index, dir) in axis_directions.iter().enumerate() {
        hit_axis_ends[index] = project(
            &view_projection,
            &viewport,
            target_position + dir * axis_world_length,
        );
    }

    let show_planes = !matches!(active_mode, ActiveMode::Rotation);
    let mut plane_handles: [Option<PlaneHandleScreen>; 3] = [None, None, None];
    if show_planes {
        for (locked_axis, slot) in plane_handles.iter_mut().enumerate() {
            *slot = compute_plane_handle_screen(
                target_position,
                &axis_directions,
                locked_axis,
                axis_world_length,
                &view_projection,
                &viewport,
            );
        }
    }

    let drag_active = drag_active_for_mode(world, active_mode);

    let head_extension = match active_mode {
        ActiveMode::Scale => sizing.scale_hit_extension_px,
        ActiveMode::LocalTranslation
        | ActiveMode::GlobalTranslation
        | ActiveMode::CompositeLocal
        | ActiveMode::CompositeGlobal => sizing.head_hit_extension_px,
        ActiveMode::Rotation => 0.0,
    };

    let hovered = if drag_active || !mouse_in_viewport || pointer_over_panel {
        HoveredHandle::None
    } else {
        compute_hovered_handle(&HoveredHandleQuery {
            mode: active_mode,
            mouse: mouse_position,
            origin: hit_origin_screen,
            axis_ends: &hit_axis_ends,
            axis_head_extension: head_extension,
            axis_hit_threshold: sizing.hit_threshold_px,
            plane_handles: &plane_handles,
            target_position,
            axis_world_length,
            axis_directions: &axis_directions,
            view_projection: &view_projection,
            viewport: &viewport,
        })
    };

    let (hovered_axis, hovered_plane, hovered_ring) = match hovered {
        HoveredHandle::Axis(index) => (Some(index), None, None),
        HoveredHandle::Plane(index) => (None, Some(index), None),
        HoveredHandle::Ring(index) => (None, None, Some(index)),
        _ => (None, None, None),
    };

    let ctx = GizmoStepContext {
        target,
        target_position,
        axis_world_length,
        axis_directions: &axis_directions,
        view_projection: &view_projection,
        inverse_view_projection: &inverse_view_projection,
        viewport: &viewport,
        camera_position,
        mouse_position,
        hit_origin_screen,
        hit_axis_ends: &hit_axis_ends,
        plane_handles: &plane_handles,
        hovered_axis,
        hovered_plane,
        hovered_ring,
        just_pressed,
        just_released,
        held,
    };

    let mut mutated = false;
    let mut active_axis_for_render: Option<usize> = None;
    let mut active_plane_for_render: Option<usize> = None;
    let mut active_ring_for_render: Option<usize> = None;
    let hovered_origin_for_render = matches!(hovered, HoveredHandle::Origin);

    match active_mode {
        ActiveMode::LocalTranslation | ActiveMode::GlobalTranslation => {
            active_plane_for_render = planar_translation_step(world, &ctx, &mut mutated);
            if active_plane_for_render.is_none() {
                active_axis_for_render = translation_step(world, &ctx, &mut mutated);
            }
        }
        ActiveMode::Scale => {
            active_axis_for_render = scale_step(world, &ctx, &mut mutated);
            if active_axis_for_render.is_none() {
                active_plane_for_render = planar_scale_step(world, &ctx, &mut mutated);
            }
        }
        ActiveMode::Rotation => {
            active_ring_for_render = rotation_step(world, &ctx, &mut mutated);
        }
        ActiveMode::CompositeLocal | ActiveMode::CompositeGlobal => {
            active_plane_for_render = planar_translation_step(world, &ctx, &mut mutated);
            if active_plane_for_render.is_none() {
                active_axis_for_render = translation_step(world, &ctx, &mut mutated);
            }
            if active_plane_for_render.is_none() && active_axis_for_render.is_none() {
                active_ring_for_render = rotation_step(world, &ctx, &mut mutated);
            }
        }
    }

    if mutated {
        crate::ecs::transform::systems::run_systems(world);
    }

    let render_origin_world = world
        .core
        .get_global_transform(target)
        .map(|transform| transform.translation())
        .unwrap_or(target_position);

    let Some(render_origin_screen) = project(&view_projection, &viewport, render_origin_world)
    else {
        world.resources.user_interface.gizmos.current_clip = None;
        return;
    };

    let render_axis_directions = world
        .core
        .get_global_transform(target)
        .map(|transform| axis_directions_for_mode(active_mode, &transform.0))
        .unwrap_or(axis_directions);

    render_handles(
        world,
        &RenderHandlesArgs {
            mode: active_mode,
            sizing,
            origin_screen: render_origin_screen,
            origin_world: render_origin_world,
            axis_directions: &render_axis_directions,
            axis_world_length,
            active_axis: active_axis_for_render,
            active_plane: active_plane_for_render,
            active_ring: active_ring_for_render,
            view_projection: &view_projection,
            viewport: &viewport,
        },
    );

    let any_active = active_axis_for_render.is_some()
        || active_plane_for_render.is_some()
        || active_ring_for_render.is_some()
        || hovered_origin_for_render;
    push_origin_marker(world, render_origin_screen, any_active);
    world.resources.user_interface.hud_wants_pointer = any_active;
    world.resources.user_interface.gizmos.current_clip = None;
}

fn clear_all_drags(world: &mut World) {
    let gizmos = &mut world.resources.user_interface.gizmos;
    gizmos.translation_drag = None;
    gizmos.scale_drag = None;
    gizmos.rotation_drag = None;
    gizmos.planar_scale_drag = None;
    gizmos.planar_translation_drag = None;
}

fn clear_inactive_drags(world: &mut World, mode: ActiveMode) {
    let gizmos = &mut world.resources.user_interface.gizmos;
    match mode {
        ActiveMode::LocalTranslation | ActiveMode::GlobalTranslation => {
            gizmos.scale_drag = None;
            gizmos.rotation_drag = None;
            gizmos.planar_scale_drag = None;
        }
        ActiveMode::Scale => {
            gizmos.translation_drag = None;
            gizmos.rotation_drag = None;
            gizmos.planar_translation_drag = None;
        }
        ActiveMode::Rotation => {
            gizmos.translation_drag = None;
            gizmos.scale_drag = None;
            gizmos.planar_scale_drag = None;
            gizmos.planar_translation_drag = None;
        }
        ActiveMode::CompositeLocal | ActiveMode::CompositeGlobal => {
            gizmos.scale_drag = None;
            gizmos.planar_scale_drag = None;
        }
    }
}

fn drag_active_for_mode(world: &World, mode: ActiveMode) -> bool {
    let gizmos = &world.resources.user_interface.gizmos;
    match mode {
        ActiveMode::LocalTranslation | ActiveMode::GlobalTranslation => {
            gizmos.translation_drag.is_some() || gizmos.planar_translation_drag.is_some()
        }
        ActiveMode::Scale => gizmos.scale_drag.is_some() || gizmos.planar_scale_drag.is_some(),
        ActiveMode::Rotation => gizmos.rotation_drag.is_some(),
        ActiveMode::CompositeLocal | ActiveMode::CompositeGlobal => {
            gizmos.translation_drag.is_some()
                || gizmos.rotation_drag.is_some()
                || gizmos.planar_translation_drag.is_some()
        }
    }
}

fn resolve_active_mode(mode: GizmoMode) -> ActiveMode {
    match mode {
        GizmoMode::LocalTranslation => ActiveMode::LocalTranslation,
        GizmoMode::GlobalTranslation => ActiveMode::GlobalTranslation,
        GizmoMode::Scale => ActiveMode::Scale,
        GizmoMode::Rotation => ActiveMode::Rotation,
        GizmoMode::CompositeLocal => ActiveMode::CompositeLocal,
        GizmoMode::CompositeGlobal => ActiveMode::CompositeGlobal,
    }
}

fn axis_directions_for_mode(mode: ActiveMode, target_matrix: &Mat4) -> [Vec3; 3] {
    match mode {
        ActiveMode::LocalTranslation
        | ActiveMode::Scale
        | ActiveMode::Rotation
        | ActiveMode::CompositeLocal => local_axes(target_matrix),
        ActiveMode::GlobalTranslation | ActiveMode::CompositeGlobal => [
            Vec3::new(1.0, 0.0, 0.0),
            Vec3::new(0.0, 1.0, 0.0),
            Vec3::new(0.0, 0.0, 1.0),
        ],
    }
}

fn local_axes(matrix: &Mat4) -> [Vec3; 3] {
    let column_dir = |index: usize| -> Vec3 {
        let direction = Vec3::new(matrix[(0, index)], matrix[(1, index)], matrix[(2, index)]);
        let magnitude = direction.magnitude();
        if magnitude < 1e-6 {
            match index {
                0 => Vec3::new(1.0, 0.0, 0.0),
                1 => Vec3::new(0.0, 1.0, 0.0),
                _ => Vec3::new(0.0, 0.0, 1.0),
            }
        } else {
            direction / magnitude
        }
    };
    [column_dir(0), column_dir(1), column_dir(2)]
}

struct GizmoStepContext<'a> {
    target: freecs::Entity,
    target_position: Vec3,
    axis_world_length: f32,
    axis_directions: &'a [Vec3; 3],
    view_projection: &'a Mat4,
    inverse_view_projection: &'a Mat4,
    viewport: &'a ViewportRect,
    camera_position: Vec3,
    mouse_position: Vec2,
    hit_origin_screen: Vec2,
    hit_axis_ends: &'a [Option<Vec2>; 3],
    plane_handles: &'a [Option<PlaneHandleScreen>; 3],
    hovered_axis: Option<usize>,
    hovered_plane: Option<usize>,
    hovered_ring: Option<usize>,
    just_pressed: bool,
    just_released: bool,
    held: bool,
}

struct RingRender<'a> {
    target_position: Vec3,
    axis_world: Vec3,
    radius_world: f32,
    color: Vec4,
    thickness: f32,
    view_projection: &'a Mat4,
    viewport: &'a ViewportRect,
}

#[derive(Clone, Copy)]
struct PlaneHandleScreen {
    corners: [Vec2; 4],
    centroid: Vec2,
    locked_axis: usize,
    center_world: Vec3,
}

struct HoveredHandleQuery<'a> {
    mode: ActiveMode,
    mouse: Vec2,
    origin: Vec2,
    axis_ends: &'a [Option<Vec2>; 3],
    axis_head_extension: f32,
    axis_hit_threshold: f32,
    plane_handles: &'a [Option<PlaneHandleScreen>; 3],
    target_position: Vec3,
    axis_world_length: f32,
    axis_directions: &'a [Vec3; 3],
    view_projection: &'a Mat4,
    viewport: &'a ViewportRect,
}

struct RenderHandlesArgs<'a> {
    mode: ActiveMode,
    sizing: GizmoSizing,
    origin_screen: Vec2,
    origin_world: Vec3,
    axis_directions: &'a [Vec3; 3],
    axis_world_length: f32,
    active_axis: Option<usize>,
    active_plane: Option<usize>,
    active_ring: Option<usize>,
    view_projection: &'a Mat4,
    viewport: &'a ViewportRect,
}

fn pointer_over_ui_panel(world: &World) -> bool {
    let pointer = world
        .resources
        .retained_ui
        .interaction_for_active()
        .hovered_entity
        .or(world
            .resources
            .retained_ui
            .interaction_for_active()
            .active_entity);
    let Some(entity) = pointer else { return false };

    let mut current = entity;
    loop {
        if world.ui.get_ui_panel(current).is_some() {
            return true;
        }
        match world.core.get_parent(current).and_then(|parent| parent.0) {
            Some(parent) => current = parent,
            None => return false,
        }
    }
}

fn compute_hovered_handle(query: &HoveredHandleQuery) -> HoveredHandle {
    match query.mode {
        ActiveMode::LocalTranslation | ActiveMode::GlobalTranslation | ActiveMode::Scale => {
            if let Some(index) = nearest_plane_handle(query.mouse, query.plane_handles) {
                return HoveredHandle::Plane(index);
            }
            if let Some(index) = nearest_axis(
                query.mouse,
                query.origin,
                query.axis_ends,
                query.axis_head_extension,
                query.axis_hit_threshold,
            ) {
                return HoveredHandle::Axis(index);
            }
            if origin_hit(query.mouse, query.origin) {
                return HoveredHandle::Origin;
            }
            HoveredHandle::None
        }
        ActiveMode::Rotation => {
            if let Some(index) = nearest_rotation_ring(
                query.mouse,
                query.target_position,
                query.axis_world_length,
                query.axis_directions,
                query.view_projection,
                query.viewport,
            ) {
                return HoveredHandle::Ring(index);
            }
            if origin_hit(query.mouse, query.origin) {
                return HoveredHandle::Origin;
            }
            HoveredHandle::None
        }
        ActiveMode::CompositeLocal | ActiveMode::CompositeGlobal => {
            if let Some(index) = nearest_plane_handle(query.mouse, query.plane_handles) {
                return HoveredHandle::Plane(index);
            }
            if let Some(index) = nearest_axis(
                query.mouse,
                query.origin,
                query.axis_ends,
                query.axis_head_extension,
                query.axis_hit_threshold,
            ) {
                return HoveredHandle::Axis(index);
            }
            if let Some(index) = nearest_rotation_ring(
                query.mouse,
                query.target_position,
                query.axis_world_length,
                query.axis_directions,
                query.view_projection,
                query.viewport,
            ) {
                return HoveredHandle::Ring(index);
            }
            if origin_hit(query.mouse, query.origin) {
                return HoveredHandle::Origin;
            }
            HoveredHandle::None
        }
    }
}

fn translation_step(
    world: &mut World,
    ctx: &GizmoStepContext,
    mutated: &mut bool,
) -> Option<usize> {
    let drag = world.resources.user_interface.gizmos.translation_drag;

    if drag.is_none()
        && ctx.just_pressed
        && let Some(axis_index) = ctx.hovered_axis
        && let Some(end) = ctx.hit_axis_ends[axis_index]
    {
        let initial_t = parametric_t_on_segment(ctx.mouse_position, ctx.hit_origin_screen, end);
        world.resources.user_interface.gizmos.translation_drag = Some(GizmoTranslationDrag {
            axis: axis_index as u8,
            initial_translation: ctx.target_position,
            axis_world_direction: ctx.axis_directions[axis_index],
            axis_world_length: ctx.axis_world_length,
            initial_t,
        });
    }

    let drag = world.resources.user_interface.gizmos.translation_drag;
    let mut active_axis = ctx.hovered_axis;
    if let Some(active) = drag {
        active_axis = Some(active.axis as usize);

        let drag_origin_screen = project(
            ctx.view_projection,
            ctx.viewport,
            active.initial_translation,
        );
        let drag_end_screen = project(
            ctx.view_projection,
            ctx.viewport,
            active.initial_translation + active.axis_world_direction * active.axis_world_length,
        );

        if ctx.held
            && let (Some(drag_origin), Some(drag_end)) = (drag_origin_screen, drag_end_screen)
        {
            let current_t = parametric_t_on_segment(ctx.mouse_position, drag_origin, drag_end);
            let mut delta_world = (current_t - active.initial_t) * active.axis_world_length;
            if let Some(step) = world.resources.user_interface.gizmos.translation_snap {
                delta_world = crate::ecs::gizmos::state::snap_value(delta_world, step);
            }
            let new_world_translation =
                active.initial_translation + active.axis_world_direction * delta_world;
            apply_world_translation(world, ctx.target, new_world_translation);
            *mutated = true;
        }

        if ctx.just_released || !ctx.held {
            world.resources.user_interface.gizmos.translation_drag = None;
        }
    }

    active_axis
}

fn scale_step(world: &mut World, ctx: &GizmoStepContext, mutated: &mut bool) -> Option<usize> {
    let drag = world.resources.user_interface.gizmos.scale_drag;

    if drag.is_none()
        && ctx.just_pressed
        && let Some(axis_index) = ctx.hovered_axis
        && let Some(end) = ctx.hit_axis_ends[axis_index]
    {
        let initial_t = parametric_t_on_segment(ctx.mouse_position, ctx.hit_origin_screen, end);
        let initial_scale = world
            .core
            .get_local_transform(ctx.target)
            .map(|transform| transform.scale)
            .unwrap_or_else(|| Vec3::new(1.0, 1.0, 1.0));
        world.resources.user_interface.gizmos.scale_drag = Some(GizmoScaleDrag {
            axis: axis_index as u8,
            initial_scale,
            axis_world_length: ctx.axis_world_length,
            initial_t,
        });
    }

    let drag = world.resources.user_interface.gizmos.scale_drag;
    let mut active_axis = ctx.hovered_axis;
    if let Some(active) = drag {
        active_axis = Some(active.axis as usize);

        let drag_origin_screen = project(ctx.view_projection, ctx.viewport, ctx.target_position);
        let drag_end_screen = project(
            ctx.view_projection,
            ctx.viewport,
            ctx.target_position
                + ctx.axis_directions[active.axis as usize] * active.axis_world_length,
        );

        if ctx.held
            && let (Some(drag_origin), Some(drag_end)) = (drag_origin_screen, drag_end_screen)
        {
            let current_t = parametric_t_on_segment(ctx.mouse_position, drag_origin, drag_end);
            let delta_t = current_t - active.initial_t;
            let scale_factor = (1.0 + delta_t * SCALE_DRAG_SENSITIVITY).max(MIN_SCALE_COMPONENT);
            let mut new_scale = active.initial_scale;
            match active.axis {
                0 => new_scale.x = active.initial_scale.x * scale_factor,
                1 => new_scale.y = active.initial_scale.y * scale_factor,
                2 => new_scale.z = active.initial_scale.z * scale_factor,
                _ => {}
            }
            new_scale.x = new_scale.x.max(MIN_SCALE_COMPONENT);
            new_scale.y = new_scale.y.max(MIN_SCALE_COMPONENT);
            new_scale.z = new_scale.z.max(MIN_SCALE_COMPONENT);
            if let Some(step) = world.resources.user_interface.gizmos.scale_snap {
                match active.axis {
                    0 => {
                        new_scale.x = crate::ecs::gizmos::state::snap_value(new_scale.x, step)
                            .max(MIN_SCALE_COMPONENT)
                    }
                    1 => {
                        new_scale.y = crate::ecs::gizmos::state::snap_value(new_scale.y, step)
                            .max(MIN_SCALE_COMPONENT)
                    }
                    2 => {
                        new_scale.z = crate::ecs::gizmos::state::snap_value(new_scale.z, step)
                            .max(MIN_SCALE_COMPONENT)
                    }
                    _ => {}
                }
            }
            apply_local_scale(world, ctx.target, new_scale);
            *mutated = true;
        }

        if ctx.just_released || !ctx.held {
            world.resources.user_interface.gizmos.scale_drag = None;
        }
    }

    active_axis
}

fn planar_scale_step(
    world: &mut World,
    ctx: &GizmoStepContext,
    mutated: &mut bool,
) -> Option<usize> {
    let drag = world.resources.user_interface.gizmos.planar_scale_drag;

    if drag.is_none()
        && ctx.just_pressed
        && let Some(locked_axis) = ctx.hovered_plane
        && let Some(plane) = ctx.plane_handles[locked_axis].as_ref()
    {
        let initial_t =
            parametric_t_on_segment(ctx.mouse_position, ctx.hit_origin_screen, plane.centroid);
        let initial_scale = world
            .core
            .get_local_transform(ctx.target)
            .map(|transform| transform.scale)
            .unwrap_or_else(|| Vec3::new(1.0, 1.0, 1.0));
        let diagonal = plane.center_world - ctx.target_position;
        let diagonal_length = diagonal.magnitude();
        if diagonal_length > 1e-4 {
            world.resources.user_interface.gizmos.planar_scale_drag = Some(GizmoPlanarScaleDrag {
                locked_axis: locked_axis as u8,
                initial_scale,
                diagonal_world: diagonal / diagonal_length,
                diagonal_world_length: diagonal_length,
                initial_t,
            });
        }
    }

    let drag = world.resources.user_interface.gizmos.planar_scale_drag;
    let mut active_plane = ctx.hovered_plane;
    if let Some(active) = drag {
        active_plane = Some(active.locked_axis as usize);

        let drag_origin_screen = project(ctx.view_projection, ctx.viewport, ctx.target_position);
        let drag_end_screen = project(
            ctx.view_projection,
            ctx.viewport,
            ctx.target_position + active.diagonal_world * active.diagonal_world_length,
        );

        if ctx.held
            && let (Some(drag_origin), Some(drag_end)) = (drag_origin_screen, drag_end_screen)
        {
            let current_t = parametric_t_on_segment(ctx.mouse_position, drag_origin, drag_end);
            let delta_t = current_t - active.initial_t;
            let scale_factor = (1.0 + delta_t * SCALE_DRAG_SENSITIVITY).max(MIN_SCALE_COMPONENT);
            let mut new_scale = active.initial_scale;
            match active.locked_axis {
                0 => {
                    new_scale.y = active.initial_scale.y * scale_factor;
                    new_scale.z = active.initial_scale.z * scale_factor;
                }
                1 => {
                    new_scale.x = active.initial_scale.x * scale_factor;
                    new_scale.z = active.initial_scale.z * scale_factor;
                }
                2 => {
                    new_scale.x = active.initial_scale.x * scale_factor;
                    new_scale.y = active.initial_scale.y * scale_factor;
                }
                _ => {}
            }
            new_scale.x = new_scale.x.max(MIN_SCALE_COMPONENT);
            new_scale.y = new_scale.y.max(MIN_SCALE_COMPONENT);
            new_scale.z = new_scale.z.max(MIN_SCALE_COMPONENT);
            if let Some(step) = world.resources.user_interface.gizmos.scale_snap {
                let snap = crate::ecs::gizmos::state::snap_value;
                match active.locked_axis {
                    0 => {
                        new_scale.y = snap(new_scale.y, step).max(MIN_SCALE_COMPONENT);
                        new_scale.z = snap(new_scale.z, step).max(MIN_SCALE_COMPONENT);
                    }
                    1 => {
                        new_scale.x = snap(new_scale.x, step).max(MIN_SCALE_COMPONENT);
                        new_scale.z = snap(new_scale.z, step).max(MIN_SCALE_COMPONENT);
                    }
                    2 => {
                        new_scale.x = snap(new_scale.x, step).max(MIN_SCALE_COMPONENT);
                        new_scale.y = snap(new_scale.y, step).max(MIN_SCALE_COMPONENT);
                    }
                    _ => {}
                }
            }
            apply_local_scale(world, ctx.target, new_scale);
            *mutated = true;
        }

        if ctx.just_released || !ctx.held {
            world.resources.user_interface.gizmos.planar_scale_drag = None;
        }
    }

    active_plane
}

fn planar_translation_step(
    world: &mut World,
    ctx: &GizmoStepContext,
    mutated: &mut bool,
) -> Option<usize> {
    let drag = world
        .resources
        .user_interface
        .gizmos
        .planar_translation_drag;

    if drag.is_none()
        && ctx.just_pressed
        && let Some(locked_axis) = ctx.hovered_plane
    {
        let plane_normal = ctx.axis_directions[locked_axis];
        if let Some(initial_world_hit) = mouse_ray_plane_hit(ctx, plane_normal) {
            world
                .resources
                .user_interface
                .gizmos
                .planar_translation_drag = Some(GizmoPlanarTranslationDrag {
                locked_axis: locked_axis as u8,
                initial_translation: ctx.target_position,
                plane_normal_world: plane_normal,
                initial_world_hit,
            });
        }
    }

    let drag = world
        .resources
        .user_interface
        .gizmos
        .planar_translation_drag;
    let mut active_plane = ctx.hovered_plane;
    if let Some(active) = drag {
        active_plane = Some(active.locked_axis as usize);

        if ctx.held
            && let Some(current_world_hit) = mouse_ray_plane_hit(ctx, active.plane_normal_world)
        {
            let mut delta = current_world_hit - active.initial_world_hit;
            if let Some(step) = world.resources.user_interface.gizmos.translation_snap {
                delta = crate::ecs::gizmos::state::snap_vec3(delta, step);
            }
            let new_world = active.initial_translation + delta;
            apply_world_translation(world, ctx.target, new_world);
            *mutated = true;
        }

        if ctx.just_released || !ctx.held {
            world
                .resources
                .user_interface
                .gizmos
                .planar_translation_drag = None;
        }
    }

    active_plane
}

fn rotation_step(world: &mut World, ctx: &GizmoStepContext, mutated: &mut bool) -> Option<usize> {
    let drag = world.resources.user_interface.gizmos.rotation_drag;

    if drag.is_none()
        && ctx.just_pressed
        && let Some(axis_index) = ctx.hovered_ring
    {
        let axis_world = ctx.axis_directions[axis_index];
        if let Some(start_point) = mouse_ray_plane_hit(ctx, axis_world) {
            let start_world_vector =
                project_onto_plane(start_point - ctx.target_position, axis_world);
            if start_world_vector.magnitude() > 1e-3 {
                let initial_rotation = world
                    .core
                    .get_local_transform(ctx.target)
                    .map(|transform| transform.rotation)
                    .unwrap_or_else(nalgebra_glm::quat_identity);
                world.resources.user_interface.gizmos.rotation_drag = Some(GizmoRotationDrag {
                    axis: axis_index as u8,
                    axis_world,
                    initial_rotation,
                    start_world_vector: start_world_vector.normalize(),
                });
            }
        }
    }

    let drag = world.resources.user_interface.gizmos.rotation_drag;
    let mut active_axis = ctx.hovered_ring;
    if let Some(active) = drag {
        active_axis = Some(active.axis as usize);

        if ctx.held
            && let Some(current_point) = mouse_ray_plane_hit(ctx, active.axis_world)
        {
            let current_vector =
                project_onto_plane(current_point - ctx.target_position, active.axis_world);
            if current_vector.magnitude() > 1e-3 {
                let current_unit = current_vector.normalize();
                let cross_vec = active.start_world_vector.cross(&current_unit);
                let dot_product = nalgebra_glm::dot(&active.start_world_vector, &current_unit);
                let mut signed_angle =
                    nalgebra_glm::dot(&cross_vec, &active.axis_world).atan2(dot_product);
                if let Some(step) = world.resources.user_interface.gizmos.rotation_snap_radians {
                    signed_angle = crate::ecs::gizmos::state::snap_value(signed_angle, step);
                }
                let delta_world = nalgebra_glm::quat_angle_axis(signed_angle, &active.axis_world);
                let new_local_rotation =
                    nalgebra_glm::quat_normalize(&(delta_world * active.initial_rotation));
                apply_local_rotation(world, ctx.target, new_local_rotation);
                *mutated = true;
            }
        }

        if ctx.just_released || !ctx.held {
            world.resources.user_interface.gizmos.rotation_drag = None;
        }
    }

    active_axis
}

fn project_onto_plane(vector: Vec3, plane_normal: Vec3) -> Vec3 {
    vector - plane_normal * nalgebra_glm::dot(&vector, &plane_normal)
}

fn mouse_ray_plane_hit(ctx: &GizmoStepContext, plane_normal: Vec3) -> Option<Vec3> {
    let ndc_x = ((ctx.mouse_position.x - ctx.viewport.x) / ctx.viewport.width) * 2.0 - 1.0;
    let ndc_y = 1.0 - ((ctx.mouse_position.y - ctx.viewport.y) / ctx.viewport.height) * 2.0;

    let near_clip = ctx.inverse_view_projection * nalgebra_glm::Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
    if near_clip.w.abs() < 1e-6 {
        return None;
    }
    let near_world = Vec3::new(near_clip.x, near_clip.y, near_clip.z) / near_clip.w;
    let direction = near_world - ctx.camera_position;
    let direction_magnitude = direction.magnitude();
    if direction_magnitude < 1e-6 {
        return None;
    }
    let direction_unit = direction / direction_magnitude;

    let denominator = nalgebra_glm::dot(&direction_unit, &plane_normal);
    if denominator.abs() < 1e-4 {
        return None;
    }
    let t = nalgebra_glm::dot(&(ctx.target_position - ctx.camera_position), &plane_normal)
        / denominator;
    if t < 0.0 {
        return None;
    }
    Some(ctx.camera_position + direction_unit * t)
}

fn compute_plane_handle_screen(
    target_position: Vec3,
    axis_directions: &[Vec3; 3],
    locked_axis: usize,
    axis_world_length: f32,
    view_projection: &Mat4,
    viewport: &ViewportRect,
) -> Option<PlaneHandleScreen> {
    let (a_index, b_index) = match locked_axis {
        0 => (1, 2),
        1 => (0, 2),
        2 => (0, 1),
        _ => return None,
    };
    let a_dir = axis_directions[a_index];
    let b_dir = axis_directions[b_index];
    let inner = axis_world_length * PLANE_HANDLE_INNER_FRACTION;
    let outer = axis_world_length * PLANE_HANDLE_OUTER_FRACTION;
    let p00 = target_position + a_dir * inner + b_dir * inner;
    let p10 = target_position + a_dir * outer + b_dir * inner;
    let p11 = target_position + a_dir * outer + b_dir * outer;
    let p01 = target_position + a_dir * inner + b_dir * outer;
    let s00 = project(view_projection, viewport, p00)?;
    let s10 = project(view_projection, viewport, p10)?;
    let s11 = project(view_projection, viewport, p11)?;
    let s01 = project(view_projection, viewport, p01)?;
    let center_world = (p00 + p10 + p11 + p01) / 4.0;
    let centroid = (s00 + s10 + s11 + s01) / 4.0;
    Some(PlaneHandleScreen {
        corners: [s00, s10, s11, s01],
        centroid,
        locked_axis,
        center_world,
    })
}

fn nearest_plane_handle(mouse: Vec2, planes: &[Option<PlaneHandleScreen>; 3]) -> Option<usize> {
    let mut best: Option<(usize, f32)> = None;
    for plane in planes.iter().flatten() {
        if !point_in_quad(mouse, &plane.corners) {
            continue;
        }
        let distance = (mouse - plane.centroid).magnitude();
        match best {
            Some((_, current)) if current <= distance => {}
            _ => best = Some((plane.locked_axis, distance)),
        }
    }
    best.map(|(index, _)| index)
}

fn point_in_quad(point: Vec2, corners: &[Vec2; 4]) -> bool {
    let mut sign: f32 = 0.0;
    for index in 0..4 {
        let from = corners[index];
        let to = corners[(index + 1) % 4];
        let edge = to - from;
        let to_point = point - from;
        let cross = edge.x * to_point.y - edge.y * to_point.x;
        if cross.abs() < 1e-4 {
            continue;
        }
        if sign == 0.0 {
            sign = cross.signum();
        } else if sign * cross < 0.0 {
            return false;
        }
    }
    true
}

fn nearest_rotation_ring(
    mouse: Vec2,
    target_position: Vec3,
    axis_world_length: f32,
    axis_directions: &[Vec3; 3],
    view_projection: &Mat4,
    viewport: &ViewportRect,
) -> Option<usize> {
    let mut best: Option<(usize, f32)> = None;
    for (axis_index, axis_world) in axis_directions.iter().copied().enumerate() {
        let basis = ring_basis(axis_world);
        let mut previous: Option<Vec2> = None;
        for sample in 0..=ROTATION_RING_SEGMENTS {
            let theta = (sample as f32 / ROTATION_RING_SEGMENTS as f32) * std::f32::consts::TAU;
            let world_point = target_position
                + (basis.0 * theta.cos() + basis.1 * theta.sin()) * axis_world_length;
            let Some(screen_point) = project(view_projection, viewport, world_point) else {
                previous = None;
                continue;
            };
            if let Some(prev) = previous {
                let distance = distance_point_to_segment(mouse, prev, screen_point);
                if distance <= ROTATION_HIT_THRESHOLD_PX {
                    match best {
                        Some((_, current)) if current <= distance => {}
                        _ => best = Some((axis_index, distance)),
                    }
                }
            }
            previous = Some(screen_point);
        }
    }
    best.map(|(index, _)| index)
}

fn push_rotation_ring(world: &mut World, ring: &RingRender) {
    let basis = ring_basis(ring.axis_world);
    let mut previous: Option<Vec2> = None;
    for sample in 0..=ROTATION_RING_SEGMENTS {
        let theta = (sample as f32 / ROTATION_RING_SEGMENTS as f32) * std::f32::consts::TAU;
        let world_point = ring.target_position
            + (basis.0 * theta.cos() + basis.1 * theta.sin()) * ring.radius_world;
        let Some(screen_point) = project(ring.view_projection, ring.viewport, world_point) else {
            previous = None;
            continue;
        };
        if let Some(prev) = previous {
            push_segment(world, prev, screen_point, ring.color, ring.thickness);
        }
        previous = Some(screen_point);
    }
}

fn ring_basis(axis: Vec3) -> (Vec3, Vec3) {
    let world_up = if axis.y.abs() > 0.9 {
        Vec3::new(1.0, 0.0, 0.0)
    } else {
        Vec3::new(0.0, 1.0, 0.0)
    };
    let u = world_up.cross(&axis).normalize();
    let v = axis.cross(&u).normalize();
    (u, v)
}

fn apply_local_rotation(
    world: &mut World,
    entity: freecs::Entity,
    new_rotation: nalgebra_glm::Quat,
) {
    if let Some(transform) = world.core.get_local_transform_mut(entity) {
        transform.rotation = new_rotation;
    }
    mark_local_transform_dirty(world, entity);
}

fn world_length_for_screen_pixels(
    projection: &crate::ecs::camera::components::Projection,
    view_distance: f32,
    viewport_height: f32,
    desired_pixels: f32,
) -> f32 {
    use crate::ecs::camera::components::Projection;
    match projection {
        Projection::Perspective(perspective) => {
            let half_height_at_distance = view_distance * (perspective.y_fov_rad * 0.5).tan();
            half_height_at_distance * 2.0 * desired_pixels / viewport_height
        }
        Projection::Orthographic(_) => desired_pixels / viewport_height,
    }
}

fn project(view_projection: &Mat4, viewport: &ViewportRect, world_position: Vec3) -> Option<Vec2> {
    let clip = view_projection
        * nalgebra_glm::Vec4::new(world_position.x, world_position.y, world_position.z, 1.0);
    if clip.w <= 0.0 {
        return None;
    }
    let ndc_x = clip.x / clip.w;
    let ndc_y = clip.y / clip.w;
    let screen_x = viewport.x + (ndc_x + 1.0) * 0.5 * viewport.width;
    let screen_y = viewport.y + (1.0 - ndc_y) * 0.5 * viewport.height;
    Some(Vec2::new(screen_x, screen_y))
}

fn nearest_axis(
    mouse: Vec2,
    origin: Vec2,
    axis_ends: &[Option<Vec2>; 3],
    head_extension: f32,
    hit_threshold: f32,
) -> Option<usize> {
    let mut best: Option<(usize, f32)> = None;
    for (index, end) in axis_ends.iter().enumerate() {
        let Some(end) = end else { continue };
        let extended_end = extend_past_tip(origin, *end, head_extension);
        let distance = distance_point_to_segment(mouse, origin, extended_end);
        if distance > hit_threshold {
            continue;
        }
        match best {
            Some((_, current)) if current <= distance => {}
            _ => best = Some((index, distance)),
        }
    }
    best.map(|(index, _)| index)
}

fn extend_past_tip(origin: Vec2, tip: Vec2, extension: f32) -> Vec2 {
    let delta = tip - origin;
    let length = delta.magnitude();
    if length < 0.001 {
        return tip;
    }
    tip + delta * (extension / length)
}

fn origin_hit(mouse: Vec2, origin: Vec2) -> bool {
    (mouse - origin).magnitude() <= ORIGIN_HIT_THRESHOLD_PX
}

fn distance_point_to_segment(point: Vec2, from: Vec2, to: Vec2) -> f32 {
    let segment = to - from;
    let length_squared = segment.norm_squared();
    if length_squared < 0.0001 {
        return (point - from).magnitude();
    }
    let t = nalgebra_glm::dot(&(point - from), &segment) / length_squared;
    let clamped = t.clamp(0.0, 1.0);
    let closest = from + segment * clamped;
    (point - closest).magnitude()
}

fn parametric_t_on_segment(point: Vec2, from: Vec2, to: Vec2) -> f32 {
    let segment = to - from;
    let length_squared = segment.norm_squared();
    if length_squared < 0.0001 {
        return 0.0;
    }
    nalgebra_glm::dot(&(point - from), &segment) / length_squared
}

fn apply_world_translation(world: &mut World, entity: freecs::Entity, new_world: Vec3) {
    let parent_inverse = world
        .core
        .get_parent(entity)
        .and_then(|parent| parent.0)
        .and_then(|parent_entity| world.core.get_global_transform(parent_entity).copied())
        .map(|parent_global| parent_global.0.try_inverse().unwrap_or_else(Mat4::identity));

    let new_local = match parent_inverse {
        Some(inverse) => {
            let homogeneous =
                inverse * nalgebra_glm::Vec4::new(new_world.x, new_world.y, new_world.z, 1.0);
            Vec3::new(homogeneous.x, homogeneous.y, homogeneous.z)
        }
        None => new_world,
    };

    if let Some(transform) = world.core.get_local_transform_mut(entity) {
        transform.translation = new_local;
    }
    mark_local_transform_dirty(world, entity);
}

fn apply_local_scale(world: &mut World, entity: freecs::Entity, new_scale: Vec3) {
    if let Some(transform) = world.core.get_local_transform_mut(entity) {
        transform.scale = new_scale;
    }
    mark_local_transform_dirty(world, entity);
}

fn render_handles(world: &mut World, args: &RenderHandlesArgs) {
    let sizing = args.sizing;

    let draw_planes = !matches!(args.mode, ActiveMode::Rotation);
    if draw_planes {
        for (locked_axis, plane_color) in PLANE_COLORS.iter().copied().enumerate() {
            let Some(plane) = compute_plane_handle_screen(
                args.origin_world,
                args.axis_directions,
                locked_axis,
                args.axis_world_length,
                args.view_projection,
                args.viewport,
            ) else {
                continue;
            };
            let is_active = args.active_plane == Some(locked_axis);
            let color = if is_active {
                PLANE_HOVER_COLOR
            } else {
                plane_color
            };
            push_plane_solid(world, &plane.corners, color);
        }
    }

    let mut axis_ends: [Option<Vec2>; 3] = [None, None, None];
    for (index, dir) in args.axis_directions.iter().enumerate() {
        axis_ends[index] = project(
            args.view_projection,
            args.viewport,
            args.origin_world + dir * args.axis_world_length,
        );
    }

    let head_kind = match args.mode {
        ActiveMode::LocalTranslation
        | ActiveMode::GlobalTranslation
        | ActiveMode::CompositeLocal
        | ActiveMode::CompositeGlobal
        | ActiveMode::Rotation => DrawHead::Triangle,
        ActiveMode::Scale => DrawHead::Box,
    };

    let draw_axes = matches!(
        args.mode,
        ActiveMode::LocalTranslation
            | ActiveMode::GlobalTranslation
            | ActiveMode::Scale
            | ActiveMode::CompositeLocal
            | ActiveMode::CompositeGlobal
    );
    if draw_axes {
        for (index, end) in axis_ends.iter().enumerate() {
            let Some(end) = end else { continue };
            let is_active = args.active_axis == Some(index);
            let color = if is_active {
                HOVER_COLOR
            } else {
                AXIS_COLORS[index]
            };
            let thickness = if is_active {
                sizing.shaft_hover_thickness_px
            } else {
                sizing.shaft_thickness_px
            };
            let head_inset = match head_kind {
                DrawHead::Triangle => sizing.head_length_px,
                DrawHead::Box => sizing.scale_box_px,
            };
            let delta = *end - args.origin_screen;
            let length = delta.magnitude();
            let segment_end = if length > head_inset {
                args.origin_screen + delta * ((length - head_inset) / length)
            } else {
                args.origin_screen
            };
            push_segment(world, args.origin_screen, segment_end, color, thickness);
            match head_kind {
                DrawHead::Triangle => push_arrow_head(
                    world,
                    args.origin_screen,
                    *end,
                    color,
                    sizing.head_length_px,
                    sizing.head_thickness_px,
                ),
                DrawHead::Box => {
                    push_scale_box(world, args.origin_screen, *end, color, sizing.scale_box_px)
                }
            }
        }
    }

    let draw_rings = matches!(
        args.mode,
        ActiveMode::Rotation | ActiveMode::CompositeLocal | ActiveMode::CompositeGlobal
    );
    if draw_rings {
        for (axis_index, axis_color) in AXIS_COLORS.iter().copied().enumerate() {
            let is_active = args.active_ring == Some(axis_index);
            let color = if is_active { HOVER_COLOR } else { axis_color };
            let thickness = if is_active {
                sizing.shaft_hover_thickness_px
            } else {
                sizing.shaft_thickness_px
            };
            push_rotation_ring(
                world,
                &RingRender {
                    target_position: args.origin_world,
                    axis_world: args.axis_directions[axis_index],
                    radius_world: args.axis_world_length,
                    color,
                    thickness,
                    view_projection: args.view_projection,
                    viewport: args.viewport,
                },
            );
        }
    }
}

fn push_segment(world: &mut World, from: Vec2, to: Vec2, color: Vec4, thickness: f32) {
    let delta = to - from;
    let length = delta.magnitude();
    if length < 0.001 {
        return;
    }
    let midpoint = (from + to) * 0.5;
    let angle = delta.y.atan2(delta.x);
    let position = midpoint - Vec2::new(length * 0.5, thickness * 0.5);
    push_rect(world, position, Vec2::new(length, thickness), color, angle);
}

fn push_arrow_head(
    world: &mut World,
    from: Vec2,
    to: Vec2,
    color: Vec4,
    head_length: f32,
    head_thickness: f32,
) {
    let delta = to - from;
    let length = delta.magnitude();
    if length < 0.001 {
        return;
    }
    let direction = delta / length;
    let head_center = to - direction * (head_length * 0.5);
    let angle = delta.y.atan2(delta.x);
    let position = head_center - Vec2::new(head_length * 0.5, head_thickness * 0.5);
    push_rect_with_effect(
        world,
        position,
        Vec2::new(head_length, head_thickness),
        color,
        angle,
        TRIANGLE_EFFECT_KIND,
    );
}

fn push_scale_box(world: &mut World, from: Vec2, to: Vec2, color: Vec4, box_px: f32) {
    let delta = to - from;
    let length = delta.magnitude();
    if length < 0.001 {
        return;
    }
    let direction = delta / length;
    let box_center = to - direction * (box_px * 0.5);
    let angle = delta.y.atan2(delta.x);
    let half = box_px * 0.5;
    let position = box_center - Vec2::new(half, half);
    push_rect(world, position, Vec2::new(box_px, box_px), color, angle);
}

fn push_plane_solid(world: &mut World, corners: &[Vec2; 4], color: Vec4) {
    let bbox_min = Vec2::new(
        corners.iter().map(|c| c.x).fold(f32::INFINITY, f32::min),
        corners.iter().map(|c| c.y).fold(f32::INFINITY, f32::min),
    );
    let bbox_max = Vec2::new(
        corners
            .iter()
            .map(|c| c.x)
            .fold(f32::NEG_INFINITY, f32::max),
        corners
            .iter()
            .map(|c| c.y)
            .fold(f32::NEG_INFINITY, f32::max),
    );
    let bbox_size = bbox_max - bbox_min;
    if bbox_size.x < 0.5 || bbox_size.y < 0.5 {
        return;
    }
    let clip = world.resources.user_interface.gizmos.current_clip;
    world.resources.retained_ui.frame.rects.push(UiRect {
        position: bbox_min,
        size: bbox_size,
        color,
        corner_radius: 0.0,
        border_width: 0.0,
        border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
        rotation: 0.0,
        clip_rect: clip,
        layer: UiLayer::Background,
        z_index: Z_INDEX,
        shadow: None,
        effect_kind: 0,
        effect_params: [0.0; 4],
        quad_corners: Some(*corners),
    });
}

fn draw_selection_marquee(world: &mut World) {
    let Some(rect) = world.resources.user_interface.gizmos.selection_marquee else {
        return;
    };
    let position = rect.min;
    let size = rect.max - rect.min;
    if size.x <= 0.0 || size.y <= 0.0 {
        return;
    }
    world.resources.retained_ui.frame.rects.push(UiRect {
        position,
        size,
        color: Vec4::new(0.30, 0.55, 1.0, 0.15),
        corner_radius: 0.0,
        border_width: 1.5,
        border_color: Vec4::new(0.45, 0.72, 1.0, 0.95),
        rotation: 0.0,
        clip_rect: None,
        layer: UiLayer::Background,
        z_index: Z_INDEX,
        shadow: None,
        effect_kind: 0,
        effect_params: [0.0; 4],
        quad_corners: None,
    });
}

fn push_origin_marker(world: &mut World, origin: Vec2, active: bool) {
    let color = if active {
        Vec4::new(1.0, 1.0, 1.0, 1.0)
    } else {
        Vec4::new(0.85, 0.85, 0.85, 1.0)
    };
    let half = ORIGIN_HANDLE_PX * 0.5;
    let position = origin - Vec2::new(half, half);
    push_rect(
        world,
        position,
        Vec2::new(ORIGIN_HANDLE_PX, ORIGIN_HANDLE_PX),
        color,
        0.0,
    );
}

fn push_rect(world: &mut World, position: Vec2, size: Vec2, color: Vec4, rotation: f32) {
    push_rect_with_effect(world, position, size, color, rotation, 0);
}

fn push_rect_with_effect(
    world: &mut World,
    position: Vec2,
    size: Vec2,
    color: Vec4,
    rotation: f32,
    effect_kind: u32,
) {
    let clip = world.resources.user_interface.gizmos.current_clip;
    world.resources.retained_ui.frame.rects.push(UiRect {
        position,
        size,
        color,
        corner_radius: 0.0,
        border_width: 0.0,
        border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
        rotation,
        clip_rect: clip,
        layer: UiLayer::Background,
        z_index: Z_INDEX,
        shadow: None,
        effect_kind,
        effect_params: [0.0; 4],
        quad_corners: None,
    });
}
