egui-gizmo 0.16.2

3d transform gizmo for the egui library
Documentation
use egui::Ui;
use glam::DVec3;

use crate::math::{round_to_interval, world_to_screen};

use crate::subgizmo::common::{
    draw_arrow, draw_circle, draw_plane, gizmo_color, gizmo_local_normal, inner_circle_radius,
    outer_circle_radius, pick_arrow, pick_circle, pick_plane, plane_bitangent, plane_tangent,
    ArrowheadStyle,
};
use crate::subgizmo::{SubGizmo, SubGizmoConfig, SubGizmoKind, TransformKind};
use crate::{GizmoDirection, GizmoMode, GizmoResult, Ray};

pub(crate) type ScaleSubGizmo = SubGizmoConfig<Scale>;

#[derive(Debug, Copy, Clone)]
pub(crate) struct ScaleParams {
    pub direction: GizmoDirection,
    pub transform_kind: TransformKind,
}

#[derive(Default, Debug, Copy, Clone)]
pub(crate) struct ScaleState {
    start_scale: DVec3,
    start_delta: f64,
}

#[derive(Default, Debug, Copy, Clone)]
pub(crate) struct Scale;

impl SubGizmoKind for Scale {
    type Params = ScaleParams;
    type State = ScaleState;
}

impl SubGizmo for ScaleSubGizmo {
    fn pick(&mut self, ui: &Ui, ray: Ray) -> Option<f64> {
        let pick_result = match (self.transform_kind, self.direction) {
            (TransformKind::Plane, GizmoDirection::View) => {
                let mut result = pick_circle(self, ray, inner_circle_radius(&self.config), true);
                if !result.picked {
                    result = pick_circle(self, ray, outer_circle_radius(&self.config), false);
                }
                result
            }
            (TransformKind::Plane, _) => pick_plane(self, ray, self.direction),
            (TransformKind::Axis, _) => pick_arrow(self, ray, self.direction),
        };

        let start_delta = distance_from_origin_2d(self, ui)?;

        self.opacity = pick_result.visibility as _;

        self.update_state_with(ui, |state: &mut ScaleState| {
            state.start_scale = self.config.scale;
            state.start_delta = start_delta;
        });

        if pick_result.picked {
            Some(pick_result.t)
        } else {
            None
        }
    }

    fn update(&mut self, ui: &Ui, _ray: Ray) -> Option<GizmoResult> {
        let state = self.state(ui);
        let mut delta = distance_from_origin_2d(self, ui)?;
        delta /= state.start_delta;

        if self.config.snapping {
            delta = round_to_interval(delta, self.config.snap_scale as f64);
        }
        delta = delta.max(1e-4) - 1.0;

        let direction = match (self.transform_kind, self.direction) {
            (TransformKind::Axis, _) => gizmo_local_normal(&self.config, self.direction),
            (TransformKind::Plane, GizmoDirection::View) => DVec3::ONE,
            (TransformKind::Plane, _) => {
                (plane_bitangent(self.direction) + plane_tangent(self.direction)).normalize()
            }
        };

        let offset = DVec3::ONE + (direction * delta);
        let new_scale = state.start_scale * offset;

        Some(GizmoResult {
            scale: new_scale.as_vec3().into(),
            rotation: self.config.rotation.as_quat().into(),
            translation: self.config.translation.as_vec3().into(),
            mode: GizmoMode::Scale,
            value: Some(offset.as_vec3().to_array()),
        })
    }

    fn draw(&mut self, ui: &Ui) {
        match (self.transform_kind, self.direction) {
            (TransformKind::Axis, _) => {
                draw_arrow(self, ui, self.direction, ArrowheadStyle::Square);
            }
            (TransformKind::Plane, GizmoDirection::View) => {
                draw_circle(
                    self,
                    ui,
                    gizmo_color(self, self.direction),
                    inner_circle_radius(&self.config),
                    false,
                );
                draw_circle(
                    self,
                    ui,
                    gizmo_color(self, self.direction),
                    outer_circle_radius(&self.config),
                    false,
                );
            }
            (TransformKind::Plane, _) => draw_plane(self, ui, self.direction),
        }
    }
}

fn distance_from_origin_2d<T: SubGizmoKind>(subgizmo: &SubGizmoConfig<T>, ui: &Ui) -> Option<f64> {
    let cursor_pos = ui.input(|i| i.pointer.hover_pos())?;
    let viewport = subgizmo.config.viewport;
    let gizmo_pos = world_to_screen(viewport, subgizmo.config.mvp, DVec3::new(0.0, 0.0, 0.0))?;

    Some(cursor_pos.distance(gizmo_pos) as f64)
}