use std::fmt::Display;
use std::ops::{Mul, MulAssign};
use godot_ffi as sys;
use sys::{ExtVariantType, GodotFfi, ffi_methods};
use crate::builtin::math::{ApproxEq, FloatExt, GlamConv, GlamType, XformInv, assert_ne_approx};
use crate::builtin::real_consts::PI;
use crate::builtin::{RAffine2, RMat2, Rect2, Vector2, real};
#[derive(Default, Copy, Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub struct Transform2D {
pub a: Vector2,
pub b: Vector2,
pub origin: Vector2,
}
impl Transform2D {
pub const IDENTITY: Self = Self::from_basis_origin(Basis2D::IDENTITY, Vector2::ZERO);
pub const FLIP_X: Self = Self::from_basis_origin(Basis2D::FLIP_X, Vector2::ZERO);
pub const FLIP_Y: Self = Self::from_basis_origin(Basis2D::FLIP_Y, Vector2::ZERO);
const fn from_basis_origin(basis: Basis2D, origin: Vector2) -> Self {
let [a, b] = basis.cols;
Self { a, b, origin }
}
pub const fn from_cols(a: Vector2, b: Vector2, origin: Vector2) -> Self {
Self { a, b, origin }
}
pub fn from_angle(angle: real) -> Self {
Self::from_angle_origin(angle, Vector2::ZERO)
}
pub fn from_angle_origin(angle: real, origin: Vector2) -> Self {
Self::from_basis_origin(Basis2D::from_angle(angle), origin)
}
pub fn from_angle_scale_skew_origin(
angle: real,
scale: Vector2,
skew: real,
origin: Vector2,
) -> Self {
Self::from_basis_origin(
Basis2D::from_cols(
Vector2::new(angle.cos(), angle.sin()),
Vector2::new(-(angle + skew).sin(), (angle + skew).cos()),
)
.scaled(scale),
origin,
)
}
#[doc(hidden)]
#[rustfmt::skip]
#[allow(clippy::too_many_arguments)]
pub const fn __internal_codegen(
ax: real, ay: real,
bx: real, by: real,
ox: real, oy: real,
) -> Self {
Self::from_cols(
Vector2::new(ax, ay),
Vector2::new(bx, by),
Vector2::new(ox, oy),
)
}
fn basis<'a>(&'a self) -> &'a Basis2D {
unsafe { std::mem::transmute::<&'a Transform2D, &'a Basis2D>(self) }
}
#[allow(clippy::wrong_self_convention)]
fn to_basis(&self) -> Basis2D {
Basis2D::from_cols(self.a, self.b)
}
#[must_use]
pub fn affine_inverse(&self) -> Self {
self.glam(|aff| aff.inverse())
}
pub fn determinant(&self) -> real {
self.basis().determinant()
}
pub fn rotation(&self) -> real {
self.basis().rotation()
}
#[must_use]
pub fn scale(&self) -> Vector2 {
self.basis().scale()
}
#[must_use]
pub fn skew(&self) -> real {
self.basis().skew()
}
#[must_use]
pub fn interpolate_with(&self, other: &Self, weight: real) -> Self {
Self::from_angle_scale_skew_origin(
self.rotation().lerp_angle(other.rotation(), weight),
self.scale().lerp(other.scale(), weight),
self.skew().lerp_angle(other.skew(), weight),
self.origin.lerp(other.origin, weight),
)
}
pub fn is_finite(&self) -> bool {
self.a.is_finite() && self.b.is_finite() && self.origin.is_finite()
}
#[must_use]
pub fn orthonormalized(&self) -> Self {
Self::from_basis_origin(self.basis().orthonormalized(), self.origin)
}
#[must_use]
pub fn rotated(&self, angle: real) -> Self {
Self::from_angle(angle) * (*self)
}
#[must_use]
pub fn rotated_local(&self, angle: real) -> Self {
(*self) * Self::from_angle(angle)
}
#[must_use]
pub fn scaled(&self, scale: Vector2) -> Self {
let mut basis = self.to_basis();
basis.set_row_a(basis.row_a() * scale.x);
basis.set_row_b(basis.row_b() * scale.y);
Self::from_basis_origin(basis, self.origin * scale)
}
#[must_use]
pub fn scaled_local(&self, scale: Vector2) -> Self {
Self::from_basis_origin(self.basis().scaled(scale), self.origin)
}
#[must_use]
pub fn translated(&self, offset: Vector2) -> Self {
Self::from_cols(self.a, self.b, self.origin + offset)
}
#[must_use]
pub fn translated_local(&self, offset: Vector2) -> Self {
Self::from_cols(self.a, self.b, self.origin + (self.to_basis() * offset))
}
pub fn basis_xform(&self, v: Vector2) -> Vector2 {
self.to_basis() * v
}
pub fn basis_xform_inv(&self, v: Vector2) -> Vector2 {
self.basis().inverse() * v
}
}
impl Display for Transform2D {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Transform2D { a, b, origin } = self;
write!(f, "[a: {a}, b: {b}, o: {origin}]")
}
}
impl Mul for Transform2D {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
self.glam2(&rhs, |a, b| a * b)
}
}
impl Mul<Vector2> for Transform2D {
type Output = Vector2;
fn mul(self, rhs: Vector2) -> Self::Output {
self.glam2(&rhs, |t, v| t.transform_point2(v))
}
}
impl XformInv<Vector2> for Transform2D {
fn xform_inv(&self, rhs: Vector2) -> Vector2 {
let v = rhs - self.origin;
self.basis_xform_inv(v)
}
}
impl Mul<real> for Transform2D {
type Output = Self;
fn mul(self, rhs: real) -> Self::Output {
Self::from_cols(self.a * rhs, self.b * rhs, self.origin * rhs)
}
}
impl Mul<Rect2> for Transform2D {
type Output = Rect2;
fn mul(self, rhs: Rect2) -> Self::Output {
let xa = self.a * rhs.position.x;
let xb = self.a * rhs.end().x;
let ya = self.b * rhs.position.y;
let yb = self.b * rhs.end().y;
let position = Vector2::coord_min(xa, xb) + Vector2::coord_min(ya, yb);
let end = Vector2::coord_max(xa, xb) + Vector2::coord_max(ya, yb);
Rect2::new(position + self.origin, end - position)
}
}
impl XformInv<Rect2> for Transform2D {
fn xform_inv(&self, rhs: Rect2) -> Rect2 {
let start = self.xform_inv(rhs.position);
let (mut min, mut max) = (start, start);
let vertices = [
Vector2::new(rhs.position.x, rhs.position.y + rhs.size.y),
rhs.end(),
Vector2::new(rhs.position.x + rhs.size.x, rhs.position.y),
];
for v in vertices {
let transformed = self.xform_inv(v);
min = Vector2::coord_min(min, transformed);
max = Vector2::coord_max(max, transformed);
}
Rect2::new(min, max - min)
}
}
impl ApproxEq for Transform2D {
#[inline]
fn approx_eq(&self, other: &Self) -> bool {
Vector2::approx_eq(&self.a, &other.a)
&& Vector2::approx_eq(&self.b, &other.b)
&& Vector2::approx_eq(&self.origin, &other.origin)
}
}
impl GlamType for RAffine2 {
type Mapped = Transform2D;
fn to_front(&self) -> Self::Mapped {
Transform2D::from_basis_origin(self.matrix2.to_front(), self.translation.to_front())
}
fn from_front(mapped: &Self::Mapped) -> Self {
Self {
matrix2: mapped.basis().to_glam(),
translation: mapped.origin.to_glam(),
}
}
}
impl GlamConv for Transform2D {
type Glam = RAffine2;
}
unsafe impl GodotFfi for Transform2D {
const VARIANT_TYPE: ExtVariantType = ExtVariantType::Concrete(sys::VariantType::TRANSFORM2D);
ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. }
}
crate::meta::impl_godot_as_self!(Transform2D: ByValue);
#[derive(Copy, Clone, PartialEq, Debug)]
#[repr(C)]
pub(crate) struct Basis2D {
cols: [Vector2; 2],
}
impl Basis2D {
pub(crate) const IDENTITY: Self = Self::from_diagonal(1.0, 1.0);
pub(crate) const FLIP_X: Self = Self::from_diagonal(-1.0, 1.0);
pub(crate) const FLIP_Y: Self = Self::from_diagonal(1.0, -1.0);
pub(crate) const fn from_diagonal(x: real, y: real) -> Self {
Self::from_cols(Vector2::new(x, 0.0), Vector2::new(0.0, y))
}
pub(crate) const fn from_cols(x: Vector2, y: Vector2) -> Self {
Self { cols: [x, y] }
}
pub(crate) fn from_angle(angle: real) -> Self {
RMat2::from_angle(angle).to_front()
}
#[must_use]
pub(crate) fn scale(&self) -> Vector2 {
let det_sign = self.determinant().signum();
Vector2::new(self.cols[0].length(), det_sign * self.cols[1].length())
}
#[must_use]
pub(crate) fn scaled(self, scale: Vector2) -> Self {
Self {
cols: [self.cols[0] * scale.x, self.cols[1] * scale.y],
}
}
pub fn determinant(&self) -> real {
self.glam(|mat| mat.determinant())
}
#[must_use]
pub fn inverse(self) -> Self {
self.glam(|mat| mat.inverse())
}
#[must_use]
pub(crate) fn orthonormalized(self) -> Self {
assert_ne_approx!(self.determinant(), 0.0, "Determinant should not be zero.");
let mut x = self.cols[0];
let mut y = self.cols[1];
x = x.normalized();
y = y - x * (x.dot(y));
y = y.normalized();
Self::from_cols(x, y)
}
pub(crate) fn rotation(&self) -> real {
real::atan2(self.cols[0].y, self.cols[0].x)
}
#[must_use]
pub(crate) fn skew(&self) -> real {
let det_sign = self.determinant().signum();
self.cols[0]
.normalized()
.dot(det_sign * self.cols[1].normalized())
.acos()
- PI * 0.5
}
pub(crate) fn set_row_a(&mut self, v: Vector2) {
self.cols[0].x = v.x;
self.cols[1].x = v.y;
}
pub(crate) fn row_a(&self) -> Vector2 {
Vector2::new(self.cols[0].x, self.cols[1].x)
}
pub(crate) fn set_row_b(&mut self, v: Vector2) {
self.cols[0].y = v.x;
self.cols[1].y = v.y;
}
pub(crate) fn row_b(&self) -> Vector2 {
Vector2::new(self.cols[0].y, self.cols[1].y)
}
}
impl Default for Basis2D {
fn default() -> Self {
Self::IDENTITY
}
}
impl Display for Basis2D {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let [a, b] = self.cols;
write!(f, "[a: {a}, b: {b})]")
}
}
impl Mul for Basis2D {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
self.glam2(&rhs, |a, b| a * b)
}
}
impl Mul<real> for Basis2D {
type Output = Self;
fn mul(self, rhs: real) -> Self::Output {
(self.to_glam() * rhs).to_front()
}
}
impl MulAssign<real> for Basis2D {
fn mul_assign(&mut self, rhs: real) {
self.cols[0] *= rhs;
self.cols[1] *= rhs;
}
}
impl Mul<Vector2> for Basis2D {
type Output = Vector2;
fn mul(self, rhs: Vector2) -> Self::Output {
self.glam2(&rhs, |a, b| a * b)
}
}
impl GlamType for RMat2 {
type Mapped = Basis2D;
fn to_front(&self) -> Self::Mapped {
Basis2D {
cols: [self.col(0).to_front(), self.col(1).to_front()],
}
}
fn from_front(mapped: &Self::Mapped) -> Self {
Self::from_cols(mapped.cols[0].to_glam(), mapped.cols[1].to_glam())
}
}
impl GlamConv for Basis2D {
type Glam = RMat2;
}
#[cfg(test)] #[cfg_attr(published_docs, doc(cfg(test)))]
mod test {
use super::*;
use crate::assert_eq_approx;
#[test]
fn transform2d_constructors_correct() {
let trans = Transform2D::from_angle(real!(115.0).to_radians());
assert_eq_approx!(trans.rotation(), real!(115.0).to_radians());
let trans =
Transform2D::from_angle_origin(real!(-80.0).to_radians(), Vector2::new(1.4, 9.8));
assert_eq_approx!(trans.rotation(), real!(-80.0).to_radians());
assert_eq_approx!(trans.origin, Vector2::new(1.4, 9.8));
let trans = Transform2D::from_angle_scale_skew_origin(
real!(170.0).to_radians(),
Vector2::new(3.6, 8.0),
real!(20.0).to_radians(),
Vector2::new(2.4, 6.8),
);
assert_eq_approx!(trans.rotation(), real!(170.0).to_radians());
assert_eq_approx!(trans.scale(), Vector2::new(3.6, 8.0));
assert_eq_approx!(trans.skew(), real!(20.0).to_radians());
assert_eq_approx!(trans.origin, Vector2::new(2.4, 6.8));
}
const DUMMY_TRANSFORM: Transform2D = Transform2D::from_basis_origin(
Basis2D::from_cols(Vector2::new(1.0, 2.0), Vector2::new(3.0, 4.0)),
Vector2::new(5.0, 6.0),
);
#[test]
fn translation() {
let offset = Vector2::new(1.0, 2.0);
assert_eq!(
Transform2D::IDENTITY.translated(offset),
Transform2D::IDENTITY.translated_local(offset)
);
let t = Transform2D::IDENTITY.translated(offset);
assert_eq!(DUMMY_TRANSFORM.translated(offset), t * DUMMY_TRANSFORM);
assert_eq!(
DUMMY_TRANSFORM.translated_local(offset),
DUMMY_TRANSFORM * t
);
}
#[test]
fn scaling() {
let scaling = Vector2::new(1.0, 2.0);
assert_eq!(
Transform2D::IDENTITY.scaled(scaling),
Transform2D::IDENTITY.scaled_local(scaling)
);
let s: Transform2D = Transform2D::IDENTITY.scaled(scaling);
assert_eq!(DUMMY_TRANSFORM.scaled(scaling), s * DUMMY_TRANSFORM);
assert_eq!(DUMMY_TRANSFORM.scaled_local(scaling), DUMMY_TRANSFORM * s);
}
#[test]
fn rotation() {
let phi = 1.0;
assert_eq!(
Transform2D::IDENTITY.rotated(phi),
Transform2D::IDENTITY.rotated_local(phi)
);
let r: Transform2D = Transform2D::IDENTITY.rotated(phi);
assert_eq!(DUMMY_TRANSFORM.rotated(phi), r * DUMMY_TRANSFORM);
assert_eq!(DUMMY_TRANSFORM.rotated_local(phi), DUMMY_TRANSFORM * r);
}
#[test]
fn interpolation() {
let rotate_scale_skew_pos: Transform2D = Transform2D::from_angle_scale_skew_origin(
real!(170.0).to_radians(),
Vector2::new(3.6, 8.0),
real!(20.0).to_radians(),
Vector2::new(2.4, 6.8),
);
let rotate_scale_skew_pos_halfway: Transform2D = Transform2D::from_angle_scale_skew_origin(
real!(85.0).to_radians(),
Vector2::new(2.3, 4.5),
real!(10.0).to_radians(),
Vector2::new(1.2, 3.4),
);
let interpolated: Transform2D =
Transform2D::IDENTITY.interpolate_with(&rotate_scale_skew_pos, 0.5);
assert_eq_approx!(interpolated.origin, rotate_scale_skew_pos_halfway.origin);
assert_eq_approx!(
interpolated.rotation(),
rotate_scale_skew_pos_halfway.rotation(),
);
assert_eq_approx!(interpolated.scale(), rotate_scale_skew_pos_halfway.scale());
assert_eq_approx!(interpolated.skew(), rotate_scale_skew_pos_halfway.skew());
assert_eq_approx!(interpolated, rotate_scale_skew_pos_halfway);
let interpolated = rotate_scale_skew_pos.interpolate_with(&Transform2D::IDENTITY, 0.5);
assert_eq_approx!(interpolated, rotate_scale_skew_pos_halfway);
}
#[test]
fn finite_number_checks() {
let x: Vector2 = Vector2::new(0.0, 1.0);
let infinite: Vector2 = Vector2::new(real::NAN, real::NAN);
assert!(
Transform2D::from_basis_origin(Basis2D::from_cols(x, x), x).is_finite(),
"let with: Transform2D all components finite should be finite",
);
assert!(
!Transform2D::from_basis_origin(Basis2D::from_cols(infinite, x), x).is_finite(),
"let with: Transform2D one component infinite should not be finite.",
);
assert!(
!Transform2D::from_basis_origin(Basis2D::from_cols(x, infinite), x).is_finite(),
"let with: Transform2D one component infinite should not be finite.",
);
assert!(
!Transform2D::from_basis_origin(Basis2D::from_cols(x, x), infinite).is_finite(),
"let with: Transform2D one component infinite should not be finite.",
);
assert!(
!Transform2D::from_basis_origin(Basis2D::from_cols(infinite, infinite), x).is_finite(),
"let with: Transform2D two components infinite should not be finite.",
);
assert!(
!Transform2D::from_basis_origin(Basis2D::from_cols(infinite, x), infinite).is_finite(),
"let with: Transform2D two components infinite should not be finite.",
);
assert!(
!Transform2D::from_basis_origin(Basis2D::from_cols(x, infinite), infinite).is_finite(),
"let with: Transform2D two components infinite should not be finite.",
);
assert!(
!Transform2D::from_basis_origin(Basis2D::from_cols(infinite, infinite), infinite)
.is_finite(),
"let with: Transform2D three components infinite should not be finite.",
);
}
#[cfg(feature = "serde")] #[cfg_attr(published_docs, doc(cfg(feature = "serde")))]
#[test]
fn serde_roundtrip() {
let transform = Transform2D::default();
let expected_json = "{\"a\":{\"x\":0.0,\"y\":0.0},\"b\":{\"x\":0.0,\"y\":0.0},\"origin\":{\"x\":0.0,\"y\":0.0}}";
crate::builtin::test_utils::roundtrip(&transform, expected_json);
}
}