roast2d_internal 0.4.0

Roast2D internal crate
Documentation
use std::f32::consts::PI;

use glam::{Mat4, Quat, Vec2, Vec3, Vec3Swizzles};

use crate::types::Rect;

impl From<Vec3> for Transform {
    fn from(pos: Vec3) -> Self {
        Self::new(pos)
    }
}

#[derive(Debug, Clone, Copy)]
pub struct Transform {
    pub pos: Vec3,
    pub scale: Vec2,
    /// 3D-compatible rotation. For 2D, this is rotation about Z.
    pub rotation: Quat,
}

impl Transform {
    pub fn new(pos: Vec3) -> Self {
        Self {
            pos,
            rotation: Quat::IDENTITY,
            scale: Vec2::ONE,
        }
    }

    /// Construct from translation only (3D-friendly), defaults to unit XY size.
    pub fn from_translation(translation: Vec3) -> Self {
        Self::new(translation)
    }

    pub fn set_xy(&mut self, Vec2 { x, y }: Vec2) {
        self.pos.x = x;
        self.pos.y = y;
    }

    pub fn with_scale(mut self, scale: Vec2) -> Self {
        self.scale = scale;
        self
    }

    pub fn with_angle(mut self, angle: f32) -> Self {
        self.rotation = Quat::from_rotation_z(angle);
        self
    }

    // No size on Transform; sizes are per-draw

    pub fn with_pos(mut self, pos: Vec3) -> Self {
        self.pos = pos;
        self
    }

    pub fn with_xy(mut self, xy: Vec2) -> Self {
        self.set_xy(xy);
        self
    }

    pub fn angle(self) -> f32 {
        self.rotation.to_euler(glam::EulerRot::XYZ).2
    }

    pub fn set_angle(&mut self, angle: f32) {
        self.rotation = Quat::from_rotation_z(angle);
    }

    // No scaled_size; use per-draw size * transform.scale

    pub fn bounds_with_anchor_and_size(&self, anchor: Vec2, size: Vec2) -> Rect {
        calc_bounds(self.pos.xy(), size, anchor, self.angle())
    }

    /// Set full 3D rotation
    pub fn with_rotation(mut self, rotation: Quat) -> Self {
        self.rotation = rotation;
        self
    }

    /// Convenience to set 3D scale while preserving 2D API
    pub fn with_scale3(mut self, scale: Vec3) -> Self {
        self.scale = scale.xy();
        self
    }

    /// Build a 4x4 model matrix combining translation, rotation (Quat), and XY scale.
    /// Z scale defaults to 1.0 to mirror 2D behavior.
    pub fn matrix(&self) -> Mat4 {
        Mat4::from_translation(self.pos)
            * Mat4::from_quat(self.rotation)
            * Mat4::from_scale(Vec3::new(self.scale.x, self.scale.y, 1.0))
    }

    // no size request helper; size is stored directly
}

fn calc_bounds(pos: Vec2, size: Vec2, anchor: Vec2, angle: f32) -> Rect {
    const HF_PI: f32 = PI * 0.5;

    // left bottom anchor
    let lb = anchor;
    // right top anchor
    let rt = Vec2::ONE - anchor;

    if angle == 0.0 || angle.abs() == PI {
        let min = pos - (size * lb);
        let max = pos + (size * rt);
        Rect { min, max }
    } else if angle.abs() == HF_PI {
        let size = Vec2 {
            x: size.y,
            y: size.x,
        };
        let min = pos - (size * lb);
        let max = pos + (size * rt);
        Rect { min, max }
    } else {
        let rot = Vec2::from_angle(angle);
        // right top
        let p_rt = size * rt;
        // left bottom
        let p_lb = -size * lb;
        // right bottom
        let p_rb = Vec2::new(p_rt.x, p_lb.y);
        // left top
        let p_lt = Vec2::new(p_lb.x, p_rt.y);
        if angle > 0. && angle < HF_PI {
            let max_x = rot.rotate(p_rb).x;
            let min_x = rot.rotate(p_lt).x;
            let max_y = rot.rotate(p_rt).y;
            let min_y = rot.rotate(p_lb).y;
            Rect {
                min: pos + Vec2::new(min_x, min_y),
                max: pos + Vec2::new(max_x, max_y),
            }
        } else if angle > HF_PI && angle < PI {
            let max_x = rot.rotate(p_lb).x;
            let min_x = rot.rotate(p_rt).x;
            let max_y = rot.rotate(p_rb).y;
            let min_y = rot.rotate(p_lt).y;
            Rect {
                min: pos + Vec2::new(min_x, min_y),
                max: pos + Vec2::new(max_x, max_y),
            }
        } else if angle > -PI && angle < -HF_PI {
            let max_x = rot.rotate(p_lt).x;
            let min_x = rot.rotate(p_rb).x;
            let max_y = rot.rotate(p_lb).y;
            let min_y = rot.rotate(p_rt).y;
            Rect {
                min: pos + Vec2::new(min_x, min_y),
                max: pos + Vec2::new(max_x, max_y),
            }
        } else if angle > -HF_PI && angle < 0.0 {
            let max_x = rot.rotate(p_rt).x;
            let min_x = rot.rotate(p_lb).x;
            let max_y = rot.rotate(p_lt).y;
            let min_y = rot.rotate(p_rb).y;
            Rect {
                min: pos + Vec2::new(min_x, min_y),
                max: pos + Vec2::new(max_x, max_y),
            }
        } else {
            panic!("Unnormalized angle {angle}")
        }
    }
}