gled-ui 2.31.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The handle above a selection that turns it, as both editors draw it: a
//! knob on a stalk above the selection box's top edge.

use crate::canvas::box_scale::drag_handle;
use crate::canvas::shapes::Shapes;
use egui::emath::TSTransform;
use egui::{Color32, Pos2, Rect, Response, Stroke, Ui, Vec2, pos2, vec2};

/// How far above the selection box the handle sits, in screen pixels.
const ROTATE_HANDLE_PX: f32 = 24.0;
/// Angle steps a turn or a drawn line keeps to while shift is held, in
/// degrees.
pub(crate) const SNAP_DEG: f32 = 15.0;

/// Draws the rotation handle above `shown_box`, which is in scene units, and
/// returns its response: a drag on it turns the selection.
pub(crate) fn rotation_handle(
    ui: &mut Ui,
    overlay: &mut Shapes,
    scene_transform: &TSTransform,
    shown_box: Rect,
) -> Response {
    let top = *scene_transform * pos2(shown_box.center().x, shown_box.min.y);
    let handle = top - vec2(0.0, ROTATE_HANDLE_PX);
    overlay.line_segment(
        [top, handle],
        Stroke::new(1.0, Color32::from_white_alpha(160)),
    );
    let id = ui.id().with("rotation handle");
    drag_handle(ui, overlay, handle, Color32::from_rgb(255, 210, 90), id)
        .on_hover_text("Turn the selection; shift snaps to 15°")
}

/// A drag on the rotation handle, anchored on the selection as it was when
/// the drag started. `P` is what each turned item looked like then.
#[derive(Debug, Clone)]
pub(crate) struct RotateDrag<P> {
    /// The point the selection turns about, in scene units.
    pub(crate) centre: Pos2,
    /// The pointer's angle about the centre when the drag started.
    pub(crate) start_angle: f32,
    /// The selection's box when the drag started; the handle stays on it.
    pub(crate) start_box: Rect,
    pub(crate) start: Vec<P>,
}

impl<P> RotateDrag<P> {
    /// A turn of the selection in `start_box` about its centre, taken hold of
    /// with the pointer at `pointer`.
    pub(crate) fn new(start_box: Rect, pointer: Pos2, start: Vec<P>) -> Self {
        let centre = start_box.center();
        Self {
            centre,
            start_angle: angle_of(pointer - centre),
            start_box,
            start,
        }
    }

    /// How far the selection has turned with the pointer at `pointer`, in
    /// radians; with `snap`, in steps of [`SNAP_DEG`].
    pub(crate) fn angle(&self, pointer: Pos2, snap: bool) -> f32 {
        turned_by(self.centre, self.start_angle, pointer, snap)
    }
}

/// The angle of `offset`, y pointing down: clockwise from three o'clock.
fn angle_of(offset: Vec2) -> f32 {
    offset.y.atan2(offset.x)
}

/// How far a selection has turned about `centre` since a drag that started
/// at `start_angle` (from [`angle_of`]) reached `pointer`, in radians; with
/// `snap`, in steps of [`SNAP_DEG`].
fn turned_by(centre: Pos2, start_angle: f32, pointer: Pos2, snap: bool) -> f32 {
    let angle = angle_of(pointer - centre) - start_angle;
    if !snap {
        return angle;
    }
    let step = SNAP_DEG.to_radians();
    (angle / step).round() * step
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::f32::consts::FRAC_PI_2;

    #[test]
    fn a_quarter_turn_of_the_pointer_turns_the_selection_a_quarter() {
        let centre = pos2(10.0, 10.0);
        let start = angle_of(pos2(20.0, 10.0) - centre);
        // y points down: from three to six o'clock is clockwise
        let angle = turned_by(centre, start, pos2(10.0, 20.0), false);
        assert!((angle - FRAC_PI_2).abs() < 1e-6, "{angle}");
    }

    #[test]
    fn shift_snaps_to_fifteen_degrees() {
        let centre = Pos2::ZERO;
        let pointer = pos2(20f32.to_radians().cos(), 20f32.to_radians().sin());
        let angle = turned_by(centre, 0.0, pointer, true);
        assert!((angle - 15f32.to_radians()).abs() < 1e-6, "{angle}");
    }
}