gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
use egui::emath::TSTransform;
use egui::{Pos2, Rect, Vec2};
use gled_model::patching::layout::scene_bounds;

/// Where a canvas is looking: the scene point in the middle of it, and how
/// many screen pixels one scene unit takes. `zoom` is `None` until the user
/// zooms, and then the canvas simply fits the scene.
///
/// Storing the visible rectangle instead ties the view to the canvas size, so
/// every resize — including the ones egui makes while measuring — feeds back
/// into the zoom and the scene jumps.
#[derive(Debug, Clone, Copy)]
pub(crate) struct SceneView {
    pub(crate) center: Pos2,
    pub(crate) zoom: Option<f32>,
}

impl SceneView {
    /// A view of the whole scene, looking at `center`.
    pub(crate) fn centred_on(center: Pos2) -> Self {
        Self { center, zoom: None }
    }
}

/// Maps the whole scene square onto the middle of `slot`, as large as it
/// fits. The live preview maps the same square onto its texture, so a
/// thumbnail, the test screen or the membership picker that used another
/// framing would show the rig at another size and place.
pub(crate) fn fit_scene_square(slot: Rect) -> TSTransform {
    let bounds = scene_bounds();
    let fitted = fit_into(bounds.size(), slot);
    let scale = fitted.width() / bounds.width();
    // TSTransform is output_pos = translation + scaling * input_pos
    TSTransform::new(
        fitted.center().to_vec2() - scale * bounds.center().to_vec2(),
        scale,
    )
}

/// Where something `size` big lands when it is fitted into `slot`: centred,
/// as large as it fits, undistorted.
pub(crate) fn fit_into(size: Vec2, slot: Rect) -> Rect {
    let scale = (slot.width() / size.x).min(slot.height() / size.y);
    Rect::from_center_size(slot.center(), size * scale)
}

#[cfg(test)]
mod tests {
    use super::*;
    use egui::{pos2, vec2};
    use gled_model::patching::layout::SCENE_SIZE;

    /// Regression: the editor drew its background, grid and thumbnails over
    /// half the scene square while the live preview mapped the whole square
    /// onto its texture, so the rig showed up at a quarter size in the
    /// preview's top-left corner.
    #[test]
    fn a_thumbnail_shows_the_same_square_the_live_preview_does() {
        let preview = Rect::from_min_size(pos2(10.0, 20.0), Vec2::splat(100.0));
        let xform = fit_scene_square(preview);
        let bounds = scene_bounds();

        assert_eq!(xform * bounds.min, preview.min);
        assert_eq!(xform * bounds.max, preview.max);
        assert_eq!(xform * bounds.center(), preview.center());

        // and the live preview's own mapping is that same square
        let uv = |p: Pos2| vec2(p.x / SCENE_SIZE, p.y / SCENE_SIZE);
        assert_eq!(uv(bounds.min), vec2(0.0, 0.0));
        assert_eq!(uv(bounds.max), vec2(1.0, 1.0));
    }

    /// The whole scene has to be on screen, or a fixture could be unpickable.
    #[test]
    fn the_scene_fits_the_space_it_is_given() {
        for rect in [
            Rect::from_min_size(pos2(10.0, 20.0), vec2(860.0, 300.0)),
            Rect::from_min_size(Pos2::ZERO, vec2(300.0, 900.0)),
            Rect::from_min_size(pos2(-40.0, 5.0), vec2(400.0, 400.0)),
        ] {
            let shown = fit_scene_square(rect) * scene_bounds();

            assert!(
                rect.expand(0.5).contains_rect(shown),
                "scene drawn at {shown:?} in {rect:?}"
            );
            assert!(
                (shown.center() - rect.center()).length() < 0.5,
                "the scene sits in the middle of the space"
            );
            assert!(
                (shown.width() - shown.height()).abs() < 0.5,
                "the scene stays square"
            );
        }
    }

    #[test]
    fn a_fitted_picture_keeps_its_shape() {
        let slot = Rect::from_min_size(Pos2::ZERO, vec2(100.0, 50.0));
        let fitted = fit_into(vec2(40.0, 40.0), slot);
        assert_eq!(
            fitted,
            Rect::from_center_size(pos2(50.0, 25.0), vec2(50.0, 50.0))
        );
    }
}